/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.gwt.user.client.ui.rta;
import org.xwiki.gwt.dom.client.CopyEvent;
import org.xwiki.gwt.dom.client.CopyHandler;
import org.xwiki.gwt.dom.client.Document;
import org.xwiki.gwt.dom.client.Event;
import org.xwiki.gwt.dom.client.HasCopyHandlers;
import org.xwiki.gwt.dom.client.HasPasteHandlers;
import org.xwiki.gwt.dom.client.JavaScriptObject;
import org.xwiki.gwt.dom.client.PasteEvent;
import org.xwiki.gwt.dom.client.PasteHandler;
import org.xwiki.gwt.user.client.ActionEvent;
import org.xwiki.gwt.user.client.ActionHandler;
import org.xwiki.gwt.user.client.HasActionHandlers;
import org.xwiki.gwt.user.client.ui.rta.cmd.CommandManager;
import org.xwiki.gwt.user.client.ui.rta.cmd.internal.DefaultCommandManager;
import org.xwiki.gwt.user.client.ui.rta.cmd.internal.DefaultCommandProvider;
import org.xwiki.gwt.user.client.ui.rta.internal.BehaviorAdjuster;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.HasDoubleClickHandlers;
import com.google.gwt.event.dom.client.HasLoadHandlers;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ClosingEvent;
import com.google.gwt.user.client.Window.ClosingHandler;
import com.google.gwt.user.client.ui.impl.RichTextAreaImpl;
/**
* Extends the rich text area provided by GWT to add support for advanced editing.
*
* @version $Id: 4a4cfb1280c7b3eabf3c7ee3aa62672a5aa1b0b8 $
*/
public class RichTextArea extends com.google.gwt.user.client.ui.RichTextArea implements HasDoubleClickHandlers,
HasLoadHandlers, LoadHandler, HasPasteHandlers, HasCopyHandlers, HasActionHandlers, ClosingHandler
{
/**
* @see #setHTML(String)
*/
public static final String DIRTY = "__dirty";
/**
* Flag indicating that the load event was fired. Ensures the rich text area is initialized only once.
*
* @see #onLoad(LoadEvent)
*/
public static final String LOADED = "__loaded";
/**
* Flag indicating that the load event is currently being handled. Ensures the rich text area is initialized only
* when the load event is fired.
*
* @see #onLoad(LoadEvent)
*/
public static final String INITIALIZING = "__initializing";
/**
* The command manager that executes commands on this rich text area.
*/
private final CommandManager cm;
/**
* Overwrites the default behavior of the rich text area when DOM events are triggered by user actions and that
* default behavior is either incomplete, unnatural, browser specific or buggy. This custom behavior can still be
* prevented from a listener by calling {@link Event#preventDefault()} on the {@link #getCurrentEvent()}.
*/
private final BehaviorAdjuster adjuster = GWT.create(BehaviorAdjuster.class);
/**
* The JavaScript function used to retrieve the edited document.
*/
private final JavaScriptObject documentGetter;
/**
* Creates a new rich text area.
*/
public RichTextArea()
{
this(new DefaultCommandManager());
new DefaultCommandProvider().provideTo(this);
}
/**
* Custom constructor allowing us to inject a mock command manager. It was mainly added to be used in unit tests.
*
* @param cm custom command manager
*/
public RichTextArea(CommandManager cm)
{
addLoadHandler(this);
this.cm = cm;
this.documentGetter = createDocumentGetter();
adjuster.setTextArea(this);
}
/**
* NOTE: If the current browser doesn't support rich text editing this method returns <code>null</code>. You should
* test the returned value and fail save to an appropriate behavior!
* <p>
* The appropriate test would be: {@code
* if (rta.isAttached() && rta.getDocument() == null) {
* // The current browser doesn't support rich text editing.
* }
* }
*
* @return The DOM document being edited with this rich text area.
*/
public native Document getDocument()
/*-{
return (this.@org.xwiki.gwt.user.client.ui.rta.RichTextArea::documentGetter)();
}-*/;
/**
* NOTE: This method was added to optimize the access to the edited document. Ideally the document getter should be
* placed in the implementation class which is browser specific but we can't add methods to the implementation base
* class and placing it in a derived class would force us to make a test which we want to avoid.
*
* @return a JavaScript function that can be used to retrieve the edited document
*/
private native JavaScriptObject createDocumentGetter()
/*-{
var outer = this;
var contentDocumentGetter = function() {
// The in-line frame element can be replaced during the life time of a rich text area so we must get it
// whenever the edited document is requested.
var element = outer.@com.google.gwt.user.client.ui.UIObject::getElement()();
// We access the content document in a static way because only static references to overlay types are
// allowed from JSNI.
return @org.xwiki.gwt.dom.client.IFrameElement::getContentDocument(Lorg/xwiki/gwt/dom/client/IFrameElement;)(element);
}
var nullDocumentGetter = function() {
return null;
}
var tagName = this.@com.google.gwt.user.client.ui.UIObject::getElement()().nodeName.toLowerCase();
return tagName == 'iframe' ? contentDocumentGetter : nullDocumentGetter;
}-*/;
/**
* @return the {@link CommandManager} associated with this instance.
*/
public CommandManager getCommandManager()
{
return cm;
}
/**
* {@inheritDoc}
* <ul>
* <li>http://code.google.com/p/google-web-toolkit/issues/detail?id=3147</li>
* <li>http://code.google.com/p/google-web-toolkit/issues/detail?id=3156</li>
* </ul>
*
* @see com.google.gwt.user.client.ui.RichTextArea#setHTML(String)
*/
@Override
public void setHTML(String html)
{
// We use a dirty flag to overcome the Issue 3156. Precisely, we test this flag in the setHTMLImpl to avoid
// overwriting the contents when setHTML haven't been called.
getElement().setPropertyBoolean(DIRTY, true);
super.setHTML(html);
}
@Override
public void onBrowserEvent(com.google.gwt.user.client.Event event)
{
// We need to preview the event due to a GWT bug.
// See http://code.google.com/p/google-web-toolkit/issues/detail?id=729.
// Note that this makes the RichTextArea unusable on a modal dialog box because the test that checks if the
// event target is a child of the panel fails for all the events triggered inside the in-line frame due to the
// fact that they come from a different document than the one holding the panel. As a result all the
// RichTextArea events are canceled when the RichTextArea is on a modal dialog box (the one provided by GWT).
// Unfortunately changing the event target to point to the in-line frame is not possible.
if (!previewEvent(event)) {
return;
}
adjuster.onBeforeBrowserEvent((Event) event);
super.onBrowserEvent(event);
adjuster.onBrowserEvent((Event) event);
}
/**
* We need to call DOM.previewEvent because there is a bug in GWT that prevents PopupPanel from previewing events
* generated in in-line frames like the one in behind of this rich text area.
* <p>
* See http://code.google.com/p/google-web-toolkit/issues/detail?id=729.
*
* @param event a handle to the event being previewed.
* @return <code>false</code> to cancel the event.
*/
private native boolean previewEvent(com.google.gwt.user.client.Event event)
/*-{
return @com.google.gwt.user.client.DOM::previewEvent(Lcom/google/gwt/user/client/Event;)(event);
}-*/;
@Override
public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler)
{
return addDomHandler(handler, DoubleClickEvent.getType());
}
@Override
public HandlerRegistration addPasteHandler(PasteHandler handler)
{
return addDomHandler(handler, PasteEvent.getType());
}
@Override
public HandlerRegistration addCopyHandler(CopyHandler handler)
{
// FIXME: We should use addDomHandler(handler, CopyEvent.getType()) but GWT currently doesn't support the copy
// event type and throws "Trying to sink unknown event type copy".
// See http://code.google.com/p/google-web-toolkit/issues/detail?id=4030 .
return addHandler(handler, CopyEvent.getType());
}
@Override
public HandlerRegistration addLoadHandler(LoadHandler handler)
{
return addDomHandler(handler, LoadEvent.getType());
}
@Override
public void onLoad(LoadEvent event)
{
// The load event could be fired multiple times.
if (!getElement().getPropertyBoolean(LOADED)) {
// Make sure the rich text area is initialized only once.
getElement().setPropertyBoolean(LOADED, true);
// Make sure the rich text area is initialized only when the load event is fired.
getElement().setPropertyBoolean(INITIALIZING, true);
try {
// The initializing flag is needed to distinguish between the case when initElement is called after the
// element is attached to the page and the case when initElement is called after the document to be
// edited is loaded.
getImpl().initElement();
} finally {
getElement().setPropertyBoolean(INITIALIZING, false);
}
}
}
/**
* NOTE: We need this method because {@link com.google.gwt.user.client.ui.RichTextArea#impl} is private.
*
* @return the underlying rich text area browser-specific implementation
*/
protected native RichTextAreaImpl getImpl()
/*-{
return this.@com.google.gwt.user.client.ui.RichTextArea::impl;
}-*/;
@Override
public HandlerRegistration addActionHandler(String actionName, ActionHandler handler)
{
return addHandler(handler, ActionEvent.getType(actionName));
}
@Override
public void sinkEvents(int eventBitsToAdd)
{
// Events listeners are not registered right away but after the widget is attached to the browser's document for
// the first time. This deferred sink behavior is not suited for the load event because the load event could be
// fired before the load listener is registered. This can happen if the underlying element is loaded
// synchronously (e.g. in-line frame with the source attribute unspecified).
if (!isOrWasAttached() && (eventBitsToAdd & Event.ONLOAD) != 0) {
// Sink the load event immediately.
DOM.sinkEvents(getElement(), eventBitsToAdd | DOM.getEventsSunk(getElement()));
DOM.setEventListener(getElement(), this);
// We can't remove the listener on detach so we listen to window closing event to remove the listener.
Window.addWindowClosingHandler(this);
} else {
// Preserve deferred sink behavior.
super.sinkEvents(eventBitsToAdd);
}
}
@Override
protected void onDetach()
{
super.onDetach();
// We need to keep the listener because onAttach is called after the rich text area is physically attached to
// the document and in some browsers the in-line frame used by the rich text area is loaded synchronously. This
// means that the load even can be fired before onAttach thus before the listener is set.
// NOTE: We have to remove the listener when the host page unloads in order to break the circular reference.
DOM.setEventListener(getElement(), this);
}
@Override
public void onWindowClosing(ClosingEvent event)
{
// We can't remove the listener on detach so we remove it here.
DOM.setEventListener(getElement(), null);
}
}