blob: b84c1c200455262ebe2ae0446b45df7c83c8c838 [file] [log] [blame]
/*
* Copyright 2011 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.uibinder.client.impl;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.shared.HasHandlers;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.uibinder.client.UiRenderer;
import java.util.HashMap;
/**
* Abstract implementation of a safe HTML binder to make implementation of generated rendering
* simpler.
*/
public abstract class AbstractUiRenderer implements UiRenderer {
/**
* Helps handle method dispatch to classes that use UiRenderer.
*
* @param <T> class that can receive events from a UiRenderer implementation
*/
protected abstract static class UiRendererDispatcher<T> implements HasHandlers {
private T eventTarget;
private int methodIndex;
private Element root;
/**
* Maps strings describing event types and field names to methods
* contained in type {@code T} (which are indexed by an integer).
*/
private HashMap<String, Integer> table;
/**
* Fire an event to the receiver.
* @param target object that will handle the events
* @param event event to dispatch
* @param parentOrRoot root element of a previously rendered DOM structure (or its parent)
*/
protected void fireEvent(T target, NativeEvent event, Element parentOrRoot) {
if (target == null) {
throw new NullPointerException("Null event handler received");
}
if (event == null) {
throw new NullPointerException("Null event object received");
}
if (parentOrRoot == null) {
throw new NullPointerException("Null parent received");
}
if (!isParentOrRenderer(parentOrRoot, RENDERED_ATTRIBUTE)) {
return;
}
eventTarget = target;
root = findRootElementOrNull(parentOrRoot, RENDERED_ATTRIBUTE);
methodIndex = computeDispatchEvent(table, root, event);
DomEvent.fireNativeEvent(event, this);
}
/**
* Object that will receive the event.
*/
protected T getEventTarget() {
return eventTarget;
}
/**
* Index of the method that will receive the event.
*/
protected int getMethodIndex() {
return methodIndex;
}
/**
* Root Element of a previously rendered DOM structure.
*/
protected Element getRoot() {
return root;
}
/**
* Initializes the dispatch table if necessary.
*/
protected void initDispatchTable(String[] keys, Integer[] values) {
table = buildDispatchMap(keys, values);
}
}
/**
* Marker attribute for DOM structures previously generated by UiRenderer.
*/
public static final String RENDERED_ATTRIBUTE = "gwtuirendered";
/**
* Field name used to identify the root element while dispatching events.
*/
public static final String ROOT_FAKE_NAME = "^";
public static final String UI_ID_SEPARATOR = ":";
private static final int NO_HANDLER_FOUND = -1;
/**
* Build id strings used to identify DOM elements related to ui:fields.
*
* @param fieldName name of the field that identifies the element
* @param uiId common part of the identifier for all elements in the rendered DOM structure
*/
protected static String buildInnerId(String fieldName, String uiId) {
return uiId + UI_ID_SEPARATOR + fieldName;
}
/**
* Retrieves a specific element within a previously rendered element.
*
* @param parent parent element containing the element of interest
* @param fieldName name of the field to retrieve
* @param attribute that identifies the root element as such
* @return the element identified by {@code fieldName}
*
* @throws IllegalArgumentException if the {@code parent} does not point to or contains
* a previously rendered element. In DevMode also when the root element is not
* attached to the DOM
* @throws IllegalStateException parent does not contain an element matching
* {@code filedName}
*
* @throws RuntimeException if the root element is not attached to the DOM and not running in
* DevMode
*
* @throws NullPointerException if {@code parent} == null
*/
protected static Element findInnerField(Element parent, String fieldName, String attribute) {
Element root = findRootElement(parent, attribute);
if (parent != root && !isRenderedElementSingleChild(root)) {
throw new IllegalArgumentException(
"Parent Element of previously rendered element contains more than one child"
+ " while getting \"" + fieldName + "\"");
}
String uiId = root.getAttribute(attribute);
String renderedId = buildInnerId(fieldName, uiId);
Element elementById = Document.get().getElementById(renderedId);
if (elementById == null) {
if (!isAttachedToDom(root)) {
throw new RuntimeException("UiRendered element is not attached to DOM while getting \""
+ fieldName + "\"");
} else if (!GWT.isProdMode()) {
throw new IllegalStateException("\"" + fieldName
+ "\" not found within rendered element");
} else {
// In prod mode we do not distinguish between being unattached or not finding the element
throw new IllegalArgumentException("UiRendered element is not attached to DOM, or \""
+ fieldName + "\" not found within rendered element");
}
}
return elementById;
}
/**
* Retrieves the root of a previously rendered element contained within the {@code parent}.
* The {@code parent} must either contain the previously rendered DOM structure as its only child,
* or point directly to the rendered element root.
*
* @param parent element containing, or pointing to, a previously rendered DOM structure
* @param attribute attribute name that identifies the root of the DOM structure
* @return the root element of the previously rendered DOM structure
*
* @throws NullPointerException if {@code parent} == null
* @throws IllegalArgumentException if {@code parent} does not contain a previously rendered
* element
*/
protected static Element findRootElement(Element parent, String attribute) {
Element root = findRootElementOrNull(parent, attribute);
if (root == null) {
throw new IllegalArgumentException(
"Parent element does not contain a previously rendered element");
}
return root;
}
/**
* Inserts an attribute into the first tag found in a {@code safeHtml} template.
* This method assumes that the {@code safeHtml} template begins with an open HTML tag.
* {@code SafeHtml} templates produced by UiBinder always meet these conditions.
* <p>
* This method does not attempt to ensure {@code atributeName} and {@code attributeValue}
* contain safe values.
*
* @returns the {@code safeHtml} template with "{@code attributeName}={@code attributeValue}"
* inserted as an attribute of the first tag found
*/
protected static SafeHtml stampUiRendererAttribute(SafeHtml safeHtml, String attributeName,
String attributeValue) {
String html = safeHtml.asString();
int endOfFirstTag = html.indexOf(">");
assert endOfFirstTag > 1 : "Safe html template does not start with an HTML open tag";
if (html.charAt(endOfFirstTag - 1) == '/') {
endOfFirstTag--;
}
html = html.substring(0, endOfFirstTag) + " " + attributeName + "=\"" + attributeValue + "\""
+ html.substring(endOfFirstTag);
return SafeHtmlUtils.fromTrustedString(html);
}
/**
* Converts an array of keys and values into a map.
*/
private static HashMap<String, Integer> buildDispatchMap(String[] keys, Integer[] values) {
HashMap<String, Integer> result = new HashMap<String, Integer>(keys.length);
for (int i = 0; i < keys.length; i++) {
result.put(keys[i], values[i]);
}
return result;
}
/**
* Obtains the index of the method that will receive an event.
* @param table event types and field names indexed by the method that
* can handle an event.
* @param root of a previously rendered DOM structure
* @param event event to handle
* @return index of the method that will process the event or NO_HANDLER_FOUND.
*/
private static int computeDispatchEvent(HashMap<String, Integer> table, Element root,
NativeEvent event) {
String uiId = root.getAttribute(RENDERED_ATTRIBUTE);
EventTarget eventTarget = event.getEventTarget();
if (!Element.is(eventTarget)) {
return NO_HANDLER_FOUND;
}
Element cursor = Element.as(eventTarget);
while (cursor != null && cursor != root && cursor.getNodeType() != Element.DOCUMENT_NODE) {
String fieldName = getFieldName(uiId, cursor);
if (fieldName == null) {
cursor = cursor.getParentElement();
continue;
}
String key = event.getType() + UI_ID_SEPARATOR + fieldName;
if (table.containsKey(key)) {
return table.get(key);
}
cursor = cursor.getParentElement();
}
if (cursor == root) {
String key = event.getType() + UI_ID_SEPARATOR + ROOT_FAKE_NAME;
if (table.containsKey(key)) {
return table.get(key);
}
}
return NO_HANDLER_FOUND;
}
/**
* Retrieves the root of a previously rendered element contained within the {@code parent}.
* The {@code parent} must either contain the previously rendered DOM structure as its only child,
* or point directly to the rendered element root.
*
* @param parent element containing, or pointing to, a previously rendered DOM structure
* @param attribute attribute name that identifies the root of the DOM structure
* @return the root element of the previously rendered DOM structure or <code>null</code>
* if {@code parent} does not contain a previously rendered element
*
* @throws NullPointerException if {@code parent} == null
*/
private static Element findRootElementOrNull(Element parent, String attribute) {
if (parent == null) {
throw new NullPointerException("parent argument is null");
}
Element rendered;
if (parent.hasAttribute(attribute)) {
// The parent is the root
return parent;
} else if ((rendered = parent.getFirstChildElement()) != null
&& rendered.hasAttribute(attribute)) {
// The first child is the root
return rendered;
} else {
return null;
}
}
/**
* Obtains the field name of a previously rendered DOM Element.
* @param uiId identifier of the fields contained in a previously rendered DOM structure
* @param element which may correspond to {@code ui:field}
* @return the field name or {@code null} if the {@code element} does not have
* an id attribute as would be produced by {@link #buildInnerId(String, String)}) with
* {@code fieldName} and {@code uiId}
*/
private static String getFieldName(String uiId, Element element) {
String id = element.getId();
if (id == null) {
return null;
}
int split = id.indexOf(UI_ID_SEPARATOR);
return split != -1 && uiId.length() == split && id.startsWith(uiId)
? id.substring(split + 1)
: null;
}
/**
* In DevMode, walks up the parents of the {@code rendered} element to ascertain that it is
* attached to the document. Always returns <code>true</code> in ProdMode.
*/
private static boolean isAttachedToDom(Element rendered) {
if (GWT.isProdMode()) {
return true;
}
Element body = Document.get().getBody();
while (rendered != null && rendered.hasParentElement() && !body.equals(rendered)) {
rendered = rendered.getParentElement();
}
return body.equals(rendered);
}
/**
* Implements {@link com.google.gwt.uibinder.client.UiRenderer#isParentOrRenderer(Element)}.
*/
private static boolean isParentOrRenderer(Element parent, String attribute) {
if (parent == null) {
return false;
}
Element root = findRootElementOrNull(parent, attribute);
return root != null && isAttachedToDom(root)
&& isRenderedElementSingleChild(root);
}
/**
* Checks that the parent of {@code rendered} has a single child.
*/
private static boolean isRenderedElementSingleChild(Element rendered) {
return GWT.isProdMode() || rendered.getParentElement().getChildCount() == 1;
}
/**
* Holds the part of the id attribute common to all elements being rendered.
*/
protected String uiId;
@Override
public boolean isParentOrRenderer(Element parent) {
return isParentOrRenderer(parent, RENDERED_ATTRIBUTE);
}
}