/**
* Copyright 2010 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 org.waveprotocol.wave.client.editor.examples.img;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Widget;
import org.waveprotocol.wave.client.common.util.DomHelper;
import org.waveprotocol.wave.client.common.util.DomHelper.JavaScriptEventListener;
import org.waveprotocol.wave.client.editor.ElementHandlerRegistry;
import org.waveprotocol.wave.client.editor.NodeEventHandler;
import org.waveprotocol.wave.client.editor.RenderingMutationHandler;
import org.waveprotocol.wave.client.editor.content.ContentElement;
import org.waveprotocol.wave.client.editor.content.ContentNode;
import org.waveprotocol.wave.client.editor.content.misc.ChunkyElementHandler;
import org.waveprotocol.wave.client.editor.content.misc.DisplayEditModeHandler;
import org.waveprotocol.wave.client.editor.content.misc.LinoTextEventHandler;
import org.waveprotocol.wave.client.editor.content.misc.UpdateContentEditable;
import org.waveprotocol.wave.client.editor.content.paragraph.ParagraphRenderer;
import org.waveprotocol.wave.client.editor.event.EditorEvent;
import org.waveprotocol.wave.client.editor.gwt.GwtRenderingMutationHandler;
import org.waveprotocol.wave.model.document.util.Point;
/**
* Demonstration doodad, with a few different variations of rendering and
* interactive behavior.
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public class MyDoodad {
public static String TAGNAME = "mydoodad";
public static String CAPTION_TAGNAME = "mycaption";
public static String REF_ATTR = "ref";
public static void register(ElementHandlerRegistry registry) {
//// For <mydoodad> ////
// Alternatives with different behaviors are commented out.
// SimpleRenderer renderer = new SimpleRenderer();
CaptionedRenderer renderer = new CaptionedRenderer();
// NodeEventHandler handler = ChunkyElementHandler.get();
// SimpleEventHandler handler = new SimpleEventHandler();
// GwtEventHandler handler = new GwtEventHandler(renderer);
CaptionedEventHandler handler = new CaptionedEventHandler(renderer);
//// For <mycaption> ////
// The next line Creates a renderer that uses a "div" for the HTML, and
// handles all the logic for correctly rendering a line
// of text. See the implementation for details.
ParagraphRenderer captionRenderer = ParagraphRenderer.create("div");
NodeEventHandler captionHandler = new CaptionEventHandler();
////
registry.registerRenderingMutationHandler(TAGNAME, renderer);
registry.registerRenderingMutationHandler(CAPTION_TAGNAME, captionRenderer);
registry.registerEventHandler(TAGNAME, handler);
registry.registerEventHandler(CAPTION_TAGNAME, captionHandler);
}
/**
* A trivial renderer that keeps the image's src attribute up-to-date with the
* model's ref attribute.
*/
static class SimpleRenderer extends RenderingMutationHandler {
@Override
public Element createDomImpl(Renderable element) {
Element imgTag = Document.get().createImageElement();
DomHelper.setContentEditable(imgTag, false, false);
return imgTag;
}
@Override
public void onActivatedSubtree(ContentElement element) {
fanoutAttrs(element);
}
@Override
public void onAttributeModified(
ContentElement element, String name, String oldValue, String newValue) {
if (REF_ATTR.equals(name)) {
element.getImplNodelet().setAttribute("src", newValue);
}
}
}
/**
* Simple event handler that allows the user to change the image.
*
* This demonstrates adding custom event handling to the doodad.
*
* We extend ChunkyElementHandler that takes care of special editor events
* that make our element have sensible behaviour with respect to arrow keys
* and delete/backspace.
*/
static class SimpleEventHandler extends ChunkyElementHandler {
@Override
public void onActivated(final ContentElement element) {
Helper.registerJsHandler(
element, element.getImplNodelet(), "click", new JavaScriptEventListener() {
@Override
public void onJavaScriptEvent(String name, Event event) {
promptNewRef(element);
}
});
}
@Override
public void onDeactivated(ContentElement element) {
// Cleanup
Helper.removeJsHandlers(element);
}
}
static void promptNewRef(ContentElement element) {
String newRef = Window.prompt("New Ref", element.getAttribute(REF_ATTR));
if (newRef != null) {
// Get the document view for mutating the persistent state, then update it
element.getMutableDoc().setElementAttribute(element, REF_ATTR, newRef);
}
}
/**
* Renderer that adds a caption and some fancier DOM rendering.
*
* Demonstrates using a GWT widget for rendering, and fancier features like
* having a concurrently-editable caption sub-element.
*/
static class CaptionedRenderer extends GwtRenderingMutationHandler {
public CaptionedRenderer() {
super(Flow.INLINE);
}
/** Gwt renderer equivalent of {@link #createDomImpl(Renderable)} */
@Override
protected CaptionedImageWidget createGwtWidget(Renderable element) {
return new CaptionedImageWidget();
}
/**
* Specify where the HTML DOM of child XML elements goes. Our widget's
* getContainer() method returns the inner 'div' where we would like to put
* the caption. We use this as the "container nodelet" so that when the
* 'mycaption' element gets added to 'mydoodad' (in the model XML), the
* caption's main 'div' nodelet automatically gets added to our doodad's
* inner container nodelet (in the render HTML).
*
* So our DOM will end up looking like this:
*
* <pre>{@literal
*
* <div class='top'> <!-- this is <mydoodad>'s top level "impl nodelet" -->
* <img src='...'/> <!-- the image inside the tag -->
* <div class='container> <!-- this is the container nodelet -->
*
* <div> <!-- this is <mycaption>'s top level impl nodelet -->
* caption text
* <br/> <!-- This br gets inserted by the paragraph renderer
* </div> and is needed on some browsers. we don't have to
* worry about it, it's taken care of for us -->
* </div>
* </div>
*
* }</pre>
*/
@Override
protected Element getContainerNodelet(Widget w) {
return ((CaptionedImageWidget) w).getContainer();
}
@Override
public void onActivatedSubtree(ContentElement element) {
super.onActivatedSubtree(element);
fanoutAttrs(element);
}
@Override
public void onAttributeModified(
ContentElement element, String name, String oldValue, String newValue) {
super.onAttributeModified(element, name, oldValue, newValue);
if (REF_ATTR.equals(name)) {
getWidget(element).setImageSrc(newValue);
}
}
/** Convenience getter */
CaptionedImageWidget getWidget(ContentElement e) {
return ((CaptionedImageWidget) getGwtWidget(e));
}
}
static class GwtEventHandler extends ChunkyElementHandler {
private final CaptionedRenderer renderer;
GwtEventHandler(CaptionedRenderer renderer) {
this.renderer = renderer;
}
@Override
public void onActivated(final ContentElement element) {
renderer.getWidget(element).setListener(new CaptionedImageWidget.Listener() {
@Override public void onClickImage() {
promptNewRef(element);
}
});
}
}
static class CaptionedEventHandler extends GwtEventHandler {
CaptionedEventHandler(CaptionedRenderer renderer) {
super(renderer);
}
/**
* Handles a left arrow that occurred with the caret immediately
* after this node, by moving caret to end of caption
*/
@Override
public boolean handleLeftAfterNode(ContentElement element, EditorEvent event) {
ContentElement caption = getCaption(element);
if (caption != null) {
// If we have a caption, move the selection into the caption
element.getSelectionHelper().setCaret(
Point.<ContentNode> end(getCaption(element)));
return true;
} else {
// If we don't have a caption, use the default behavior
return super.handleLeftAfterNode(element, event);
}
}
/**
* Similar to {@link #handleLeftAfterNode(ContentElement, EditorEvent)}
*/
@Override
public boolean handleRightBeforeNode(ContentElement element, EditorEvent event) {
ContentElement caption = getCaption(element);
if (caption != null) {
// If we have a caption, move the selection into the caption
element.getSelectionHelper().setCaret(
Point.start(element.getRenderedContentView(), caption));
return true;
} else {
// If we don't have a caption, use the default behavior
return super.handleRightBeforeNode(element, event);
}
}
/**
* Handles a left arrow at the beginning of the caption, moving the
* selection out of the whole doodad. We receive this event because the
* caption doesn't handle it and it bubbles outwards to our handler here.
*/
@Override
public boolean handleLeftAtBeginning(ContentElement element, EditorEvent event) {
// NOTE: The use of location mapper will normalise into text nodes.
element.getSelectionHelper().setCaret(element.getLocationMapper().getLocation(
Point.before(element.getRenderedContentView(), element)));
return true;
}
/**
* Similar to {@link #handleLeftAtBeginning(ContentElement, EditorEvent)}
*/
@Override
public boolean handleRightAtEnd(ContentElement element, EditorEvent event) {
// NOTE: The use of location mapper will normalise into text nodes.
element.getSelectionHelper().setCaret(element.getLocationMapper().getLocation(
Point.after(element.getRenderedContentView(), element)));
return true;
}
private ContentElement getCaption(ContentElement element) {
return (ContentElement) element.getFirstChild();
}
}
/**
* Event handler for our caption. Demonstrates two things:
* 1. Subclassing LinoTextEventHandler, which provides sane behavior for,
* well, a line-of-text. (See its code for details)
* 2. Use of utility to synchronise editability of caption region with main
* editor region.
*/
static class CaptionEventHandler extends LinoTextEventHandler {
@Override
public void onActivated(ContentElement element) {
super.onActivated(element);
// Add a listener to edit mode changes.
// We use an existing one that does exactly what we want: updates the editability of
// our element's container as a result.
DisplayEditModeHandler.setEditModeListener(element, UpdateContentEditable.get());
}
}
}