package org.waveprotocol.wave.client.doodad.widget;
import java.util.Date;
import org.waveprotocol.wave.client.common.util.DomHelper;
import org.waveprotocol.wave.client.common.util.JsoStringMap;
import org.waveprotocol.wave.client.doodad.widget.jso.JsoWidget;
import org.waveprotocol.wave.client.doodad.widget.jso.JsoWidgetController;
import org.waveprotocol.wave.client.editor.EditorStaticDeps;
import org.waveprotocol.wave.client.editor.ElementHandlerRegistry;
import org.waveprotocol.wave.client.editor.NodeEventHandlerImpl;
import org.waveprotocol.wave.client.editor.RenderingMutationHandler;
import org.waveprotocol.wave.client.editor.content.CMutableDocument;
import org.waveprotocol.wave.client.editor.content.ContentElement;
import org.waveprotocol.wave.client.editor.content.ContentNode;
import org.waveprotocol.wave.client.editor.event.EditorEvent;
import org.waveprotocol.wave.model.document.util.DocHelper;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.document.util.XmlStringBuilder;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.StringMap;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
/**
* Doodad for generic Widgets. It delegates rendering and behavior to
* native JavaScript methods passed as {@link JsoWidgetController}
*
* A widget has two attributes:
*
* <li>Type: a string constant to for each widget type</li>
* <li>State: the shared state stored in the document</li>
*
* @author pablojan@gmail.com (Pablo Ojanguren)
*
*/
public class WidgetDoodad {
public static final String TAG = "widget";
public static final String CSS_CLASS = "widget";
public static final String ATTR_STATE = "state";
public static final String ATTR_TYPE = "type";
public static final String ATTR_ID = "id";
static class WidgetRendererHandler extends RenderingMutationHandler {
final StringMap<JsoWidgetController> controllers;
public WidgetRendererHandler(StringMap<JsoWidgetController> controllers) {
this.controllers = controllers;
}
@Override
public Element createDomImpl(Renderable element) {
Element widgetElement = Document.get().createDivElement();
widgetElement.addClassName(CSS_CLASS);
// DomHelper.setContentEditable(widgetElement, false, false);
return widgetElement;
}
@Override
public void onActivatedSubtree(ContentElement element) {
String state = element.getAttribute(ATTR_STATE);
String type = element.getAttribute(ATTR_TYPE);
String typeForRender = type.replace("/", "-");
String id = element.getAttribute(ATTR_ID);
element.getImplNodelet().addClassName(typeForRender);
element.getImplNodelet().setId(id);
element.getImplNodelet().setAttribute("data-"+typeForRender, state);
JsoWidgetController controller = controllers.get(type);
if (controller != null) {
controller.onInit(element.getImplNodelet(), state);
}
}
@Override
public void onAttributeModified(ContentElement element, String name, String oldValue,
String newValue) {
String type = element.getAttribute(ATTR_TYPE);
if (name.equals(ATTR_STATE)) {
JsoWidgetController controller = controllers.get(type);
if (controller != null) {
controller.onChangeState(element.getImplNodelet(), oldValue, newValue);
}
}
}
@Override
public void onRemovedFromParent(ContentElement element, ContentElement newParent) {
}
}
static class WidgetEventHandler extends NodeEventHandlerImpl {
final StringMap<JsoWidgetController> controllers;
public WidgetEventHandler(StringMap<JsoWidgetController> controllers) {
this.controllers = controllers;
}
@Override
public void onActivated(ContentElement element) {
String type = element.getAttribute(ATTR_TYPE);
JsoWidgetController controller = controllers.get(type);
if (controller != null)
controller.onActivated(element.getImplNodelet());
}
@Override
public void onDeactivated(ContentElement element) {
String type = element.getAttribute(ATTR_TYPE);
JsoWidgetController controller = controllers.get(type);
if (controller != null)
controller.onDeactivated(element.getImplNodelet());
}
/**
* Removes the entire widget
*
* {@inheritDoc}
*/
@Override
public boolean handleDeleteBeforeNode(ContentElement element, EditorEvent event) {
EditorStaticDeps.logger.trace().log("Delete before widget", element);
element.getMutableDoc().deleteNode(element);
return true;
}
/**
* Removes the entire widget
*
* {@inheritDoc}
*/
@Override
public boolean handleBackspaceAfterNode(ContentElement element, EditorEvent event) {
EditorStaticDeps.logger.trace().log("Backspace after widget", element);
element.getMutableDoc().deleteNode(element);
return true;
}
/**
* Handles a left arrow that occurred with the caret immediately
* after this node, by moving caret right before the widget
*
* {@inheritDoc}
*/
@Override
public boolean handleLeftAfterNode(ContentElement element, EditorEvent event) {
element.getSelectionHelper().setCaret(Point.before(element.getContext().document(), element));
return true;
}
/**
* Handles a right arrow that occurred with the caret immediately
* before this node, by moving caret to right after the widget
*
* {@inheritDoc}
*/
@Override
public boolean handleRightBeforeNode(ContentElement element, EditorEvent event) {
element.getSelectionHelper().setCaret(Point.after(element.getContext().document(), element));
return true;
}
@Override
public boolean handleLeftAtBeginning(ContentElement element, EditorEvent event) {
return false;
}
@Override
public boolean handleRightAtEnd(ContentElement element, EditorEvent event) {
return false;
}
}
private static StringMap<JsoWidgetController> widgetControllers = JsoStringMap.<JsoWidgetController>create();
public static void register(ElementHandlerRegistry registry,
StringMap<JsoWidgetController> controllers) {
widgetControllers = controllers;
WidgetRendererHandler renderer = new WidgetRendererHandler(controllers);
WidgetEventHandler eventHandler = new WidgetEventHandler(controllers);
registry.registerRenderingMutationHandler(TAG, renderer);
registry.registerEventHandler(TAG, eventHandler);
}
/**
* Insert or append a widget in a document
*
* @param doc the mutable document
* @param point point where to insert the widget or null to append
* @param type type of the widget
* @param state initial state of the widget
*/
public static ContentElement addWidget(CMutableDocument doc, Point<ContentNode> point, String type, String state) {
Preconditions.checkArgument(doc != null, "Unable to add widget to a null document");
Preconditions.checkArgument(type != null && widgetControllers.containsKey(type),
"Widget type is not registered");
// For now, the widget id will be a timestamp
String id = type.replace("/", "-") + "-" + String.valueOf(new Date().getTime());
XmlStringBuilder xml = XmlStringBuilder
.createFromXmlString("<"+TAG+" "+ATTR_TYPE+"='" + type + "' "+ATTR_STATE+"='" + state + "' "+ATTR_ID+"='" + id + "' />");
ContentElement element = null;
if (point != null)
element = doc.insertXml(point, xml);
else
element = doc.appendXml(xml);
return element != null ? element : null;
}
/**
* Do a recursive search bottom-up in the DOM searching the first node matching
* the class
*
* @param element
* @param clazz
* @return
*/
public static Element getWidgetElementUp(Element element) {
if (element == null)
return null;
if (element.hasClassName(CSS_CLASS)) {
return element;
}
return getWidgetElementUp(element.getParentElement());
}
/**
* Helper method to get a native view of widget based on its DOM element or descendant.
*
* @param doc the mutable document
* @param domElement the widget element or a descendant
*/
public static JsoWidget getWidget(CMutableDocument doc, Element domElement) {
Element widgetDomElement = getWidgetElementUp(domElement);
if (widgetDomElement == null)
return null;
String widgetId = widgetDomElement.getAttribute(ATTR_ID);
ContentElement widgetElement = DocHelper.findElementById(doc, doc.getDocumentElement(), widgetId);
if (widgetElement == null)
return null;
return JsoWidget.create(widgetElement.getImplNodelet(), widgetElement);
}
}