RichTextArea widget implementation.



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@839 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
new file mode 100644
index 0000000..4de2308
--- /dev/null
+++ b/user/src/com/google/gwt/user/RichText.gwt.xml
@@ -0,0 +1,48 @@
+<!--                                                                        -->

+<!-- Copyright 2006 Google Inc. All Rights Reserved.                        -->

+<!-- Deferred binding rules for browser selection.                          -->

+<!--                                                                        -->

+<module>

+	<inherits name="com.google.gwt.core.Core" />

+	<inherits name="com.google.gwt.user.UserAgent" />

+	<inherits name="com.google.gwt.i18n.I18N" />

+

+	<!-- IE-specific implementation -->

+	<replace-with

+		class="com.google.gwt.user.client.ui.impl.RichTextAreaImplIE6">

+		<when-type-is

+			class="com.google.gwt.user.client.ui.impl.RichTextAreaImpl" />

+		<when-property-is name="user.agent" value="ie6" />

+	</replace-with>

+

+	<!-- Mozilla-specific implementation -->

+	<replace-with

+		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>

+	</replace-with>

+

+	<!-- Safari-specific implementation -->

+	<replace-with

+		class="com.google.gwt.user.client.ui.impl.RichTextAreaImplSafari">

+		<when-type-is

+			class="com.google.gwt.user.client.ui.impl.RichTextAreaImpl" />

+		<any>

+			<when-property-is name="user.agent" value="safari" />

+		</any>

+	</replace-with>

+

+	<!-- Opera-specific implementation -->

+	<replace-with

+		class="com.google.gwt.user.client.ui.impl.RichTextAreaImplOpera">

+		<when-type-is

+			class="com.google.gwt.user.client.ui.impl.RichTextAreaImpl" />

+		<any>

+			<when-property-is name="user.agent" value="opera" />

+		</any>

+	</replace-with>

+</module>

diff --git a/user/src/com/google/gwt/user/User.gwt.xml b/user/src/com/google/gwt/user/User.gwt.xml
index 3a84ffd..aa07463 100644
--- a/user/src/com/google/gwt/user/User.gwt.xml
+++ b/user/src/com/google/gwt/user/User.gwt.xml
@@ -29,5 +29,6 @@
    <inherits name="com.google.gwt.user.Focus"/>
    <inherits name="com.google.gwt.user.ImageBundle"/>  
    <inherits name="com.google.gwt.user.ClippedImage"/>
+   <inherits name="com.google.gwt.user.RichText"/>
    <inherits name="com.google.gwt.user.SplitPanel"/>
 </module>
diff --git a/user/src/com/google/gwt/user/client/ui/RichTextArea.java b/user/src/com/google/gwt/user/client/ui/RichTextArea.java
new file mode 100644
index 0000000..fae4aad
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/RichTextArea.java
@@ -0,0 +1,443 @@
+/*

+ * Copyright 2007 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;

+

+import com.google.gwt.core.client.GWT;

+import com.google.gwt.user.client.DOM;

+import com.google.gwt.user.client.Event;

+import com.google.gwt.user.client.ui.impl.RichTextAreaImpl;

+

+/**

+ * A rich text editor that allows complex styling and formatting.

+ * 

+ * Because some browsers do not support rich text editing, and others support

+ * only a limited subset of functionality, there are two formatter interfaces,

+ * accessed via {@link #getBasicFormatter()} and {@link #getExtendedFormatter()}. A

+ * browser that does not support rich text editing at all will return

+ * <code>null</code> for both of these, while one that supports only the basic

+ * functionality will return <code>null</code> for the latter.

+ * 

+ * <p>

+ * <img class='gallery' src='RichTextArea.png'/>

+ * </p>

+ * 

+ * <h3>CSS Style Rules</h3>

+ * <ul class="css">

+ * <li>.gwt-RichTextArea { }</li>

+ * </ul>

+ */

+public class RichTextArea extends FocusWidget implements HasHTML,

+    SourcesMouseEvents, SourcesChangeEvents {

+

+  /**

+   * 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.

+   */

+  public interface BasicFormatter {

+

+    /**

+     * Gets the background color.

+     * 

+     * @return the background color

+     */

+    String getBackColor();

+

+    /**

+     * Gets the foreground color.

+     * 

+     * @return the foreground color

+     */

+    String getForeColor();

+

+    /**

+     * Is the current region bold?

+     * 

+     * @return true if the current region is bold

+     */

+    boolean isBold();

+

+    /**

+     * Is the current region italic?

+     * 

+     * @return true if the current region is italic

+     */

+    boolean isItalic();

+

+    /**

+     * Is the current region subscript?

+     * 

+     * @return true if the current region is subscript

+     */

+    boolean isSubscript();

+

+    /**

+     * Is the current region superscript?

+     * 

+     * @return true if the current region is superscript

+     */

+    boolean isSuperscript();

+

+    /**

+     * Is the current region underlined?

+     * 

+     * @return true if the current region is underlined

+     */

+    boolean isUnderlined();

+

+    /**

+     * Sets the background color.

+     * 

+     * @param color the new background color

+     */

+    void setBackColor(String color);

+

+    /**

+     * Sets the font name.

+     * 

+     * @param name the new font name

+     */

+    void setFontName(String name);

+

+    /**

+     * Sets the font size.

+     * 

+     * @param fontSize the new font size

+     */

+    void setFontSize(FontSize fontSize);

+

+    /**

+     * Sets the foreground color.

+     * 

+     * @param color the new foreground color

+     */

+    void setForeColor(String color);

+

+    /**

+     * Sets the justification.

+     * 

+     * @param justification the new justification

+     */

+    void setJustification(Justification justification);

+

+    /**

+     * Toggles bold.

+     */

+    void toggleBold();

+

+    /**

+     * Toggles italic.

+     */

+    void toggleItalic();

+

+    /**

+     * Toggles subscript.

+     */

+    void toggleSubscript();

+

+    /**

+     * Toggles superscript.

+     */

+    void toggleSuperscript();

+

+    /**

+     * Toggles underline.

+     */

+    void toggleUnderline();

+

+    /**

+     * Selects all the text.

+     */

+    void selectAll();

+  }

+

+  /**

+   * Font size enumeration. Represents the seven basic HTML font sizes, as

+   * defined in CSS.

+   */

+  public static class FontSize {

+

+    /**

+     * Represents an XX-Small font.

+     */

+    public static final FontSize XX_SMALL = new FontSize(1);

+

+    /**

+     * Represents an X-Small font.

+     */

+    public static final FontSize X_SMALL = new FontSize(2);

+

+    /**

+     * Represents a Small font.

+     */

+    public static final FontSize SMALL = new FontSize(3);

+

+    /**

+     * Represents a Medium font.

+     */

+    public static final FontSize MEDIUM = new FontSize(4);

+

+    /**

+     * Represents a Large font.

+     */

+    public static final FontSize LARGE = new FontSize(5);

+

+    /**

+     * Represents an X-Large font.

+     */

+    public static final FontSize X_LARGE = new FontSize(6);

+

+    /**

+     * Represents an XX-Large font.

+     */

+    public static final FontSize XX_LARGE = new FontSize(7);

+

+    private int number;

+

+    private FontSize(int number) {

+      this.number = number;

+    }

+

+    /**

+     * Gets the HTML font number associated with this font size.

+     * 

+     * @return an integer from 1 to 7 inclusive

+     */

+    public int getNumber() {

+      return number;

+    }

+  }

+

+  /**

+   * 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.

+   */

+  public interface ExtendedFormatter extends BasicFormatter {

+

+    /**

+     * Creates a link to the supplied URL.

+     * 

+     * @param url the URL to be linked to

+     */

+    void createLink(String url);

+

+    /**

+     * Inserts a horizontal rule.

+     */

+    void insertHorizontalRule();

+

+    /**

+     * Inserts an image element.

+     * 

+     * @param url the url of the image to be inserted

+     */

+    void insertImage(String url);

+

+    /**

+     * Starts an numbered list. Indentation will create nested items.

+     */

+    void insertOrderedList();

+

+    /**

+     * Starts an bulleted list. Indentation will create nested items.

+     */

+    void insertUnorderedList();

+

+    /**

+     * Is the current region strikethrough?

+     * 

+     * @return true if the current region is strikethrough

+     */

+    boolean isStrikethrough();

+

+    /**

+     * Left indent.

+     */

+    void leftIndent();

+

+    /**

+     * Removes all formatting on the selected text.

+     */

+    void removeFormat();

+

+    /**

+     * Removes any link from the selected text.

+     */

+    void removeLink();

+

+    /**

+     * Right indent.

+     */

+    void rightIndent();

+

+    /**

+     * Toggles strikethrough.

+     */

+    void toggleStrikethrough();

+  }

+

+  /**

+   * Justification enumeration. The three values are <code>left</code>,

+   * <code>right</code>, <code>center</code>.

+   */

+  public static class Justification {

+

+    /**

+     * Center justification.

+     */

+    public static final Justification CENTER = new Justification("Center");

+

+    /**

+     * Left justification.

+     */

+    public static final Justification LEFT = new Justification("Left");

+

+    /**

+     * Right justification.

+     */

+    public static final Justification RIGHT = new Justification("Right");

+

+    private String tag;

+

+    private Justification(String tag) {

+      this.tag = tag;

+    }

+

+    public String toString() {

+      return "Justify " + tag;

+    }

+  }

+

+  private RichTextAreaImpl impl = (RichTextAreaImpl) GWT.create(RichTextAreaImpl.class);

+  private ChangeListenerCollection changeListeners;

+  private MouseListenerCollection mouseListeners;

+

+  /**

+   * Creates a new, blank {@link RichTextArea} object with no stylesheet.

+   */

+  public RichTextArea() {

+    setElement(impl.getElement());

+    setStyleName("gwt-RichTextArea");

+  }

+

+  public void addChangeListener(ChangeListener listener) {

+    if (changeListeners == null) {

+      changeListeners = new ChangeListenerCollection();

+    }

+    changeListeners.add(listener);

+  }

+

+  public void addMouseListener(MouseListener listener) {

+    if (mouseListeners == null) {

+      mouseListeners = new MouseListenerCollection();

+    }

+    mouseListeners.add(listener);

+  }

+

+  /**

+   * Gets the basic rich text formatting interface.

+   * 

+   * @return <code>null</code> if basic formatting is not supported

+   */

+  public BasicFormatter getBasicFormatter() {

+    if ((impl instanceof BasicFormatter) && (impl.isBasicEditingSupported())) {

+      return (BasicFormatter) impl;

+    }

+    return null;

+  }

+

+  /**

+   * Gets the full rich text formatting interface.

+   * 

+   * @return <code>null</code> if full formatting is not supported

+   */

+  public ExtendedFormatter getExtendedFormatter() {

+    if ((impl instanceof ExtendedFormatter)

+        && (impl.isExtendedEditingSupported())) {

+      return (ExtendedFormatter) impl;

+    }

+    return null;

+  }

+

+  public String getHTML() {

+    return impl.getHTML();

+  }

+

+  public String getText() {

+    return impl.getText();

+  }

+

+  public void onBrowserEvent(Event event) {

+    switch (DOM.eventGetType(event)) {

+      case Event.ONMOUSEDOWN:

+      case Event.ONMOUSEUP:

+      case Event.ONMOUSEMOVE:

+      case Event.ONMOUSEOVER:

+      case Event.ONMOUSEOUT:

+        if (mouseListeners != null) {

+          mouseListeners.fireMouseEvent(this, event);

+        }

+        break;

+

+      case Event.ONCHANGE:

+        if (changeListeners != null) {

+          changeListeners.fireChange(this);

+        }

+        break;

+

+      default:

+        super.onBrowserEvent(event);

+    }

+  }

+

+  public void removeChangeListener(ChangeListener listener) {

+    if (changeListeners != null) {

+      changeListeners.remove(listener);

+    }

+  }

+

+  public void removeMouseListener(MouseListener listener) {

+    if (mouseListeners != null) {

+      mouseListeners.remove(listener);

+    }

+  }

+

+  public void setFocus(boolean focused) {

+    impl.setFocus(focused);

+  }

+

+  public void setHTML(String html) {

+    impl.setHTML(html);

+  }

+

+  public void setText(String text) {

+    impl.setText(text);

+  }

+

+  protected void onAttach() {

+    super.onAttach();

+    impl.initElement();

+    impl.hookEvents(this);

+  }

+

+  protected void onDetach() {

+    super.onDetach();

+    impl.unhookEvents(this);

+  }

+}

diff --git a/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImpl.java b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImpl.java
new file mode 100644
index 0000000..ca0c0ff
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImpl.java
@@ -0,0 +1,87 @@
+/*

+ * Copyright 2007 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.DOM;

+import com.google.gwt.user.client.Element;

+import com.google.gwt.user.client.Event;

+import com.google.gwt.user.client.ui.RichTextArea;

+

+/**

+ * Base class for RichText platform implementations. The default version

+ * simply creates a text area with no rich text support.

+ */

+public class RichTextAreaImpl {

+

+  protected Element elem;

+

+  public RichTextAreaImpl() {

+    elem = createElement();

+  }

+

+  public Element getElement() {

+    return elem;

+  }

+

+  public String getHTML() {

+    return DOM.getAttribute(elem, "value");

+  }

+

+  public String getText() {

+    return DOM.getAttribute(elem, "value");

+  }

+

+  public void hookEvents(RichTextArea owner) {

+    DOM.setEventListener(elem, owner);

+  }

+

+  public void initElement() {

+    DOM.sinkEvents(elem, Event.MOUSEEVENTS | Event.KEYEVENTS | Event.ONCHANGE

+        | Event.ONCLICK);

+  }

+

+  public boolean isBasicEditingSupported() {

+    return false;

+  }

+

+  public boolean isExtendedEditingSupported() {

+    return false;

+  }

+

+  public native void setFocus(boolean focused) /*-{

+    if (focused) {

+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.focus();

+    } else {

+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.blur();

+    } 

+  }-*/;

+

+  public void setHTML(String html) {

+    DOM.setAttribute(elem, "value", html);

+  }

+

+  public void setText(String text) {

+    DOM.setAttribute(elem, "value", text);

+  }

+

+  public void unhookEvents(RichTextArea owner) {

+    DOM.setEventListener(elem, null);

+  }

+

+  protected Element createElement() {

+    return DOM.createTextArea();

+  }

+}

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
new file mode 100644
index 0000000..153c42d
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplIE6.java
@@ -0,0 +1,64 @@
+/*

+ * Copyright 2007 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;

+

+/**

+ * IE6-specific implementation of rich-text editing.

+ */

+public class RichTextAreaImplIE6 extends RichTextAreaImplStandard {

+

+  public native void initElement() /*-{

+    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;

+    var _this = this;

+

+    elem.onload = function() {

+      var doc = elem.contentWindow.document; 

+      doc.designMode = 'On';

+

+      doc.open();

+      doc.write('<html><body></body></html>');

+      doc.close();

+

+      // Initialize event handling.

+      _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplIE6::initEvents()();

+    };

+  }-*/;

+

+  native void initEvents() /*-{

+    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;

+    var handler = function(evt) {

+      if (elem.__listener) {

+        elem.__listener.

+          @com.google.gwt.user.client.ui.RichTextArea::onBrowserEvent(Lcom/google/gwt/user/client/Event;)(evt);

+      }

+    };

+

+    var body = elem.contentWindow.document.body;

+    body.attachEvent('onkeydown', handler);

+    body.attachEvent('onkeyup', handler);

+    body.attachEvent('onkeypress', handler);

+    body.attachEvent('onmousedown', handler);

+    body.attachEvent('onmouseup', handler);

+    body.attachEvent('onmousemove', handler);

+    body.attachEvent('onmouseover', handler);

+    body.attachEvent('onmouseout', handler);

+    body.attachEvent('onclick', handler);

+  }-*/;

+

+  public native String getText() /*-{

+    return this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.innerText;

+  }-*/;

+}

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
new file mode 100644
index 0000000..1984e62
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplMozilla.java
@@ -0,0 +1,28 @@
+/*

+ * Copyright 2007 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;

+

+/**

+ * Mozilla-specific implementation of rich-text editing.

+ */

+public class RichTextAreaImplMozilla extends RichTextAreaImplStandard {

+

+  public void setBackColor(String color) {

+    // Gecko uses 'BackColor' for the *entire area's* background. 'HiliteColor'

+    // does what we actually want.

+    execCommand("HiliteColor", color);

+  }

+}

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
new file mode 100644
index 0000000..bff1cb9
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplOpera.java
@@ -0,0 +1,152 @@
+/*

+ * Copyright 2007 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.DOM;

+import com.google.gwt.user.client.Element;

+import com.google.gwt.user.client.ui.RichTextArea;

+

+/**

+ * Opera implementation of rich-text editing.

+ */

+public class RichTextAreaImplOpera extends RichTextAreaImplStandard {

+

+  private static boolean supportsEditing;

+

+  public Element createElement() {

+    supportsEditing = detectEditingSupport();

+    if (supportsEditing) {

+      return super.createElement();

+    }

+    return DOM.createTextArea();

+  }

+

+  public String getHTML() {

+    if (supportsEditing) {

+      return super.getHTML();

+    }

+

+    // Unsupported fallback.

+    return ((RichTextAreaImpl) this).getHTML();

+  }

+

+  public String getText() {

+    if (supportsEditing) {

+      return super.getText();

+    }

+

+    // Unsupported fallback.

+    return ((RichTextAreaImpl) this).getText();

+  }

+

+  public void hookEvents(RichTextArea owner) {

+    if (supportsEditing) {

+      super.hookEvents(owner);

+      return;

+    }

+

+    // Unsupported fallback.

+    ((RichTextAreaImpl) this).hookEvents(owner);

+  }

+

+  public void initElement() {

+    if (supportsEditing) {

+      super.initElement();

+      return;

+    }

+

+    // Unsupported fallback.

+    ((RichTextAreaImpl) this).initElement();

+  }

+

+  public boolean isBasicEditingSupported() {

+    return supportsEditing;

+  }

+

+  public boolean isFullEditingSupported() {

+    return supportsEditing;

+  }

+

+  public void setBackColor(String color) {

+    // Opera uses 'BackColor' for the *entire area's* background. 'HiliteColor'

+    // does what we actually want.

+    execCommand("HiliteColor", color);

+  }

+

+  public native void setFocus(boolean focused) /*-{

+    // Opera needs the *iframe* focused, not its window.

+    if (focused) {

+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.focus();

+    } else {

+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.blur();

+    }

+  }-*/;

+

+  public void setHTML(String html) {

+    if (supportsEditing) {

+      super.setHTML(html);

+    }

+

+    // Unsupported fallback.

+    DOM.setAttribute(elem, "value", html);

+  }

+

+  public void setText(String text) {

+    if (supportsEditing) {

+      super.setText(text);

+    }

+

+    // Unsupported fallback.

+    DOM.setAttribute(elem, "value", text);

+  }

+

+  public void unhookEvents(RichTextArea owner) {

+    if (supportsEditing) {

+      super.unhookEvents(owner);

+      return;

+    }

+

+    // Unsupported fallback.

+    ((RichTextAreaImpl) this).unhookEvents(owner);

+  }

+

+  void execCommand(String cmd, String param) {

+    if (isRichEditingActive(elem)) {

+      // Opera doesn't need focus for execCommand() to work, but focusing

+      // the editor causes it to lose its selection, so we focus *after*

+      // execCommand().

+      execCommandAssumingFocus(cmd, param);

+      setFocus(true);

+

+      // TODO: Opera has now lost its selection. Figure out a way to restore it

+      // reliably.

+    }

+  }

+

+  boolean queryCommandState(String cmd) {

+    // Opera doesn't need focus (and setting it dumps selection).

+    return queryCommandStateAssumingFocus(cmd);

+  }

+

+  String queryCommandValue(String cmd) {

+    // Opera doesn't need focus (and setting it dumps selection).

+    return queryCommandValueAssumingFocus(cmd);

+  }

+

+  private native boolean detectEditingSupport() /*-{

+    return !!$doc.designMode;

+  }-*/;

+}

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
new file mode 100644
index 0000000..5569548
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplSafari.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2007 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.Element;
+import com.google.gwt.user.client.ui.RichTextArea.FontSize;
+
+/**
+ * Safari rich text platform implementation.
+ */
+public class RichTextAreaImplSafari extends RichTextAreaImplStandard {
+
+  private static final String[] sizeNumberCSSValues = new String[] {
+      "medium", "xx-small", "x-small", "small", "medium", "large", "x-large",
+      "xx-large"};
+
+  public Element createElement() {
+    Element elem = super.createElement();
+
+    // Use this opportunity to check if this version of Safari has full rich
+    // text support or not.
+    capabilityTest(elem);
+    return elem;
+  }
+
+  public native boolean isBold() /*-{
+    return !!this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.__gwt_isBold;
+  }-*/;
+
+  public native boolean isExtendedEditingSupported() /*-{
+    // __gwt_fullSupport is set in testCapability().
+    return this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.__gwt_fullSupport;
+  }-*/;
+
+  public native boolean isItalic() /*-{
+    return !!this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.__gwt_isItalic;
+  }-*/;
+
+  public native boolean isUnderlined() /*-{
+    return !!this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.__gwt_isUnderlined;
+  }-*/;
+
+  public native void setFocus(boolean focused) /*-{
+    // Opera needs the *iframe* focused, not its window.
+    if (focused) {
+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.focus();
+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.__gwt_restoreSelection();
+    } else {
+      this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.blur();
+    }
+  }-*/;
+
+  public void setFontSize(FontSize fontSize) {
+    // Safari only accepts css-style 'small, medium, large, etc' values.
+    int number = fontSize.getNumber();
+    if ((number >= 0) && (number <= 7)) {
+      execCommand("FontSize", sizeNumberCSSValues[number]);
+      return;
+    }
+  }
+
+  native void initEvents() /*-{
+    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;
+    var wnd = elem.contentWindow;
+    var doc = wnd.document;
+
+    // Create an expando on the element to hold the selection state.
+    elem.__gwt_selection = { baseOffset:0, extentOffset:0, baseNode:null, extentNode:null };
+
+    // A function for restoring the selection state.
+    elem.__gwt_restoreSelection = function() {
+      var sel = elem.__gwt_selection;
+      wnd.getSelection().setBaseAndExtent(sel.baseNode, sel.baseOffset, sel.extentNode, sel.extentOffset);
+    };
+
+    // Generic event dispatcher. Also stores selection state.
+    var handler = function(evt) {
+      // Store the editor's selection state.
+      var s = wnd.getSelection();
+      elem.__gwt_selection = {
+        baseOffset:s.baseOffset,
+        extentOffset:s.extentOffset,
+
+        baseNode:s.baseNode,
+        extentNode:s.extentNode
+      };
+
+      // Hang on to bold/italic/underlined states.
+      elem.__gwt_isBold = doc.queryCommandState('Bold');
+      elem.__gwt_isItalic = doc.queryCommandState('Italic');
+      elem.__gwt_isUnderlined = doc.queryCommandState('Underline');
+
+      // Dispatch the event.
+      if (elem.__listener) {
+        elem.__listener.
+        @com.google.gwt.user.client.ui.RichTextArea::onBrowserEvent(Lcom/google/gwt/user/client/Event;)(evt);
+      }
+    };
+
+    wnd.addEventListener('keydown', handler, true);
+    wnd.addEventListener('keyup', handler, true);
+    wnd.addEventListener('keypress', handler, true);
+    wnd.addEventListener('mousedown', handler, true);
+    wnd.addEventListener('mouseup', handler, true);
+    wnd.addEventListener('mousemove', handler, true);
+    wnd.addEventListener('mouseover', handler, true);
+    wnd.addEventListener('mouseout', handler, true);
+    wnd.addEventListener('click', handler, true);
+  }-*/;
+
+  private native void capabilityTest(Element elem) /*-{
+    elem.__gwt_fullSupport = $doc.queryCommandSupported('insertimage');
+  }-*/;
+}
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
new file mode 100644
index 0000000..6db11ae
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/RichTextAreaImplStandard.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2007 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.Element;
+import com.google.gwt.user.client.ui.RichTextArea;
+import com.google.gwt.user.client.ui.RichTextArea.FontSize;
+import com.google.gwt.user.client.ui.RichTextArea.Justification;
+
+/**
+ * Basic rich text platform implementation.
+ */
+public class RichTextAreaImplStandard extends RichTextAreaImpl implements
+    RichTextArea.BasicFormatter, RichTextArea.ExtendedFormatter {
+
+  public native Element createElement() /*-{
+    return $doc.createElement('iframe');
+  }-*/;
+
+  public void createLink(String url) {
+    execCommand("CreateLink", url);
+  }
+
+  public String getBackColor() {
+    return queryCommandValue("BackColor");
+  }
+
+  public String getForeColor() {
+    return queryCommandValue("ForeColor");
+  }
+
+  public native String getHTML() /*-{
+    return this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.innerHTML;
+  }-*/;
+
+  public native String getText() /*-{
+    return this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.textContent;
+  }-*/;
+
+  public native void hookEvents(RichTextArea owner) /*-{
+    this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.__listener = owner;
+  }-*/;
+
+  public native void initElement() /*-{
+    // Some browsers don't like setting designMode until slightly _after_
+    // the iframe becomes attached to the DOM. Any non-zero timeout will do
+    // just fine.
+    var _this = this;
+    setTimeout(function() {
+      // Turn on design mode.
+      _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.designMode = 'On';
+
+      // Initialize event handling.
+      _this.@com.google.gwt.user.client.ui.impl.RichTextAreaImplStandard::initEvents()();
+    }, 1);
+  }-*/;
+
+  public void insertHorizontalRule() {
+    execCommand("InsertHorizontalRule", null);
+  }
+
+  public void insertImage(String url) {
+    execCommand("InsertImage", url);
+  }
+
+  public void insertOrderedList() {
+    execCommand("InsertOrderedList", null);
+  }
+
+  public void insertUnorderedList() {
+    execCommand("InsertUnorderedList", null);
+  }
+
+  public boolean isBasicEditingSupported() {
+    return true;
+  }
+
+  public boolean isBold() {
+    return queryCommandState("Bold");
+  }
+
+  public boolean isExtendedEditingSupported() {
+    return true;
+  }
+
+  public boolean isItalic() {
+    return queryCommandState("Italic");
+  }
+
+  public boolean isStrikethrough() {
+    return queryCommandState("Strikethrough");
+  }
+
+  public boolean isSubscript() {
+    return queryCommandState("Subscript");
+  }
+
+  public boolean isSuperscript() {
+    return queryCommandState("Superscript");
+  }
+
+  public boolean isUnderlined() {
+    return queryCommandState("Underline");
+  }
+
+  public void leftIndent() {
+    execCommand("Outdent", null);
+  }
+
+  public void removeFormat() {
+    execCommand("RemoveFormat", null);
+  }
+
+  public void removeLink() {
+    execCommand("Unlink", "false");
+  }
+
+  public void rightIndent() {
+    execCommand("Indent", null);
+  }
+
+  public void selectAll() {
+    execCommand("SelectAll", null);
+  }
+
+  public void setBackColor(String color) {
+    execCommand("BackColor", color);
+  }
+
+  public native void setFocus(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();
+    } 
+  }-*/;
+
+  public void setFontName(String name) {
+    execCommand("FontName", name);
+  }
+
+  public void setFontSize(FontSize fontSize) {
+    execCommand("FontSize", Integer.toString(fontSize.getNumber()));
+  }
+
+  public void setForeColor(String color) {
+    execCommand("ForeColor", color);
+  }
+
+  public native void setHTML(String html) /*-{
+    this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.innerHTML = html;
+  }-*/;
+
+  public void setJustification(Justification justification) {
+    if (justification == Justification.CENTER) {
+      execCommand("JustifyCenter", null);
+    } else if (justification == Justification.LEFT) {
+      execCommand("JustifyLeft", null);
+    } else if (justification == Justification.RIGHT) {
+      execCommand("JustifyRight", null);
+    }
+  }
+
+  public native void setText(String text) /*-{
+    this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.body.textContent = text;
+  }-*/;
+
+  public void toggleBold() {
+    execCommand("Bold", "false");
+  }
+
+  public void toggleItalic() {
+    execCommand("Italic", "false");
+  }
+
+  public void toggleStrikethrough() {
+    execCommand("Strikethrough", "false");
+  }
+
+  public void toggleSubscript() {
+    execCommand("Subscript", "false");
+  }
+
+  public void toggleSuperscript() {
+    execCommand("Superscript", "false");
+  }
+
+  public void toggleUnderline() {
+    execCommand("Underline", "False");
+  }
+
+  public native void unhookEvents(RichTextArea owner) /*-{
+    this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.__listener = null;
+  }-*/;
+
+  void execCommand(String cmd, String param) {
+    if (isRichEditingActive(elem)) {
+      // 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);
+    }
+  }
+
+  native void execCommandAssumingFocus(String cmd, String param) /*-{
+    this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.execCommand(cmd, false, param);
+  }-*/;
+
+  native void initEvents() /*-{
+    var elem = this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem;
+    var handler = function(evt) {
+      if (elem.__listener) {
+        elem.__listener.@com.google.gwt.user.client.ui.RichTextArea::onBrowserEvent(Lcom/google/gwt/user/client/Event;)(evt);
+      }
+    };
+
+    var wnd = elem.contentWindow;
+    wnd.addEventListener('keydown', handler, true);
+    wnd.addEventListener('keyup', handler, true);
+    wnd.addEventListener('keypress', handler, true);
+    wnd.addEventListener('mousedown', handler, true);
+    wnd.addEventListener('mouseup', handler, true);
+    wnd.addEventListener('mousemove', handler, true);
+    wnd.addEventListener('mouseover', handler, true);
+    wnd.addEventListener('mouseout', handler, true);
+    wnd.addEventListener('click', handler, true);
+  }-*/;
+
+  native boolean isRichEditingActive(Element e) /*-{
+    return ((e.contentWindow.document.designMode).toUpperCase()) == 'ON';
+  }-*/;
+
+  boolean queryCommandState(String cmd) {
+    if (isRichEditingActive(elem)) {
+      // 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;
+    }
+  }
+
+  native boolean queryCommandStateAssumingFocus(String cmd) /*-{
+    return !!this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.queryCommandState(cmd);
+  }-*/;
+
+  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);
+  }
+
+  native String queryCommandValueAssumingFocus(String cmd) /*-{
+    return this.@com.google.gwt.user.client.ui.impl.RichTextAreaImpl::elem.contentWindow.document.queryCommandValue(cmd);
+  }-*/;
+}