| /* |
| * 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); |
| } |
| } |