blob: 580f0815a1c6d00e0d5c422a2778047fdad6a575 [file] [log] [blame]
/*
* Copyright 2008 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.user.client.ui;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.safehtml.shared.annotations.SuppressIsSafeHtmlCastCheck;
import com.google.gwt.user.client.DOM;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* A panel that contains HTML, and which can attach child widgets to identified
* elements within that HTML.
*/
public class HTMLPanel extends ComplexPanel {
private static Element hiddenDiv;
/**
* A helper method for creating unique IDs for elements within dynamically-
* generated HTML. This is important because no two elements in a document
* should have the same id.
*
* @return a new unique identifier
*/
public static String createUniqueId() {
return Document.get().createUniqueId();
}
/**
* Creates an HTML panel that wraps an existing element.
*
* This element must already be attached to the document. If the element is
* removed from the document, you must call
* {@link RootPanel#detachNow(Widget)}.
*
* @param element the element to be wrapped
*/
public static HTMLPanel wrap(Element element) {
// Assert that the element is attached.
assert Document.get().getBody().isOrHasChild(element);
HTMLPanel html = new HTMLPanel(element);
// Mark it attached and remember it for cleanup.
html.onAttach();
RootPanel.detachOnWindowClose(html);
return html;
}
/**
* Creates an HTML panel with the specified HTML contents inside a DIV
* element. Any element within this HTML that has a specified id can contain a
* child widget.
*
* @param html the panel's HTML
*/
public HTMLPanel(@IsSafeHtml String html) {
/*
* Normally would call this("div", html), but that method
* has some slightly expensive IE defensiveness that we just
* don't need for a div
*/
setElement(Document.get().createDivElement());
getElement().setInnerHTML(html);
}
/**
* Initializes the panel's HTML from a given {@link SafeHtml} object.
*
* Similar to {@link #HTMLPanel(String)}
*
* @param safeHtml the html to set.
*/
public HTMLPanel(SafeHtml safeHtml) {
this(safeHtml.asString());
}
/**
* Creates an HTML panel whose root element has the given tag, and with the
* specified HTML contents. Any element within this HTML that has a specified
* id can contain a child widget.
*
* @param tag the tag of the root element
* @param html the panel's HTML
*/
@SuppressIsSafeHtmlCastCheck
public HTMLPanel(String tag, @IsSafeHtml String html) {
// Optimization for when the HTML is empty.
if ("".equals(html)) {
setElement(Document.get().createElement(tag));
return;
}
/*
* IE has very arbitrary rules about what will and will not accept
* innerHTML. <table> and <tbody> simply won't, the property is read only.
* <p> will explode if you incorrectly try to put another <p> inside of it.
* And who knows what else.
*
* However, if you cram a complete, possibly incorrect structure inside a
* div, IE will swallow it gladly. So that's what we do here in the name of
* IE robustification.
*/
StringBuilder b = new StringBuilder();
b.append('<').append(tag).append('>').append(html);
b.append("</").append(tag).append('>');
// We could use the static hiddenDiv, but that thing is attached
// to the document. The caller might not want that.
DivElement scratchDiv = Document.get().createDivElement();
scratchDiv.setInnerHTML(b.toString());
setElement(scratchDiv.getFirstChildElement());
getElement().removeFromParent();
}
/**
* Construct a new {@link HTMLPanel} with the specified element.
*
* @param elem the element at the root of the panel
*/
private HTMLPanel(Element elem) {
setElement(elem);
}
/**
* Adds a child widget to the panel.
*
* @param widget the widget to be added
*/
@Override
public void add(Widget widget) {
add(widget, getElement());
}
/**
* Adds a child widget to the panel, contained within the HTML element
* specified by a given id.
*
* @param widget the widget to be added
* @param id the id of the element within which it will be contained
*/
public void add(Widget widget, String id) {
final Element elem = getElementById(id);
if (elem == null) {
throw new NoSuchElementException(id);
}
add(widget, elem);
}
/**
* Adds a child widget to the panel, contained within an HTML
* element. It is up to the caller to ensure that the given element
* is a child of this panel's root element.
*
* @param widget the widget to be added
* @param elem the element within which it will be contained
*/
@Override
public void add(Widget widget, Element elem) {
super.add(widget, elem);
}
/**
* Adds a child widget to the panel, replacing the HTML element.
*
* @param widget the widget to be added
* @param toReplace the element to be replaced by the widget
*/
public final void addAndReplaceElement(Widget widget, Element toReplace) {
addAndReplaceElement(widget, DOM.asOld(toReplace));
}
/**
* Adds a child widget to the panel, replacing the HTML element.
*
* @param widget the widget to be added
* @param toReplace the element to be replaced by the widget
* @deprecated use {@link #addAndReplaceElement(Widget, Element)}
*/
@Deprecated
public void addAndReplaceElement(Widget widget, com.google.gwt.user.client.Element toReplace) {
/*
* Early exit if the element to replace and the replacement are the same. If
* we remove the new widget, we would also remove the element to replace.
*/
if (toReplace == widget.getElement()) {
return;
}
// Logic pulled from super.add(), replacing the element rather than adding.
// Detach new child. Okay if its a child of the element to replace.
widget.removeFromParent();
// Logical detach of all children of the element to replace.
Widget toRemove = null;
Iterator<Widget> children = getChildren().iterator();
while (children.hasNext()) {
Widget next = children.next();
if (toReplace.isOrHasChild(next.getElement())) {
if (next.getElement() == toReplace) {
/*
* If the element that we are replacing is itself a widget, then we
* cannot remove it until the new widget has been inserted, or we lose
* the location of the element to replace. Save the widget to remove
* for now, and remove it after inserting the new widget.
*/
toRemove = next;
break;
}
children.remove();
}
}
// Logical attach.
getChildren().add(widget);
// Physical attach.
if (toRemove == null) {
toReplace.getParentNode().replaceChild(widget.getElement(), toReplace);
} else {
/*
* The element being replaced is a widget, which needs to be removed.
* First insert the new widget at the same location, then remove the old
* widget.
*/
toReplace.getParentNode().insertBefore(widget.getElement(), toReplace);
remove(toRemove);
}
// Adopt.
adopt(widget);
}
/**
* Overloaded version for IsWidget.
*
* @see #addAndReplaceElement(Widget,Element)
*
* @deprecated use {@link #addAndReplaceElement(IsWidget, Element)}
*/
@Deprecated
public void addAndReplaceElement(IsWidget widget,
com.google.gwt.user.client.Element toReplace) {
this.addAndReplaceElement(widget.asWidget(), toReplace);
}
/**
* Overloaded version for IsWidget.
*
* @see #addAndReplaceElement(Widget,Element)
*/
public void addAndReplaceElement(IsWidget widget, Element toReplace) {
this.addAndReplaceElement(widget.asWidget(), toReplace);
}
/**
* Adds a child widget to the panel, replacing the HTML element specified by a
* given id.
*
* @param widget the widget to be added
* @param id the id of the element to be replaced by the widget
*/
public void addAndReplaceElement(Widget widget, String id) {
final Element toReplace = getElementById(id);
if (toReplace == null) {
throw new NoSuchElementException(id);
}
addAndReplaceElement(widget, toReplace);
}
/**
* Overloaded version for IsWidget.
*
* @see #addAndReplaceElement(Widget,String)
*/
public void addAndReplaceElement(IsWidget widget, String id) {
this.addAndReplaceElement(widget.asWidget(), id);
}
/**
* Finds an {@link Element element} within this panel by its id.
*
* This method uses
* {@link com.google.gwt.dom.client.Document#getElementById(String)}, so the
* id must still be unique within the document.
*
* @param id the id of the element to be found
* @return the element with the given id, or <code>null</code> if none is found
*/
public com.google.gwt.user.client.Element getElementById(String id) {
Element elem = isAttached() ? Document.get().getElementById(id) : attachToDomAndGetElement(id);
return DOM.asOld(elem);
}
/**
* Performs a {@link Document#getElementById(String)} after attaching the panel's
* element into a hidden DIV in the document's body. Attachment is necessary
* to be able to use the native getElementById. The panel's element will be
* re-attached to its original parent (if any) after the method returns.
*
* @param id the id whose associated element is to be retrieved
* @return the associated element, or <code>null</code> if none is found
*/
private Element attachToDomAndGetElement(String id) {
// If the hidden DIV has not been created, create it.
if (hiddenDiv == null) {
hiddenDiv = Document.get().createDivElement();
UIObject.setVisible(hiddenDiv, false);
RootPanel.getBodyElement().appendChild(hiddenDiv);
}
// Hang on to the panel's original parent and sibling elements so that it
// can be replaced.
Element origParent = getElement().getParentElement();
Element origSibling = getElement().getNextSiblingElement();
// Attach the panel's element to the hidden div.
hiddenDiv.appendChild(getElement());
// Now that we're attached to the DOM, we can use getElementById.
Element child = Document.get().getElementById(id);
// Put the panel's element back where it was.
if (origParent != null) {
origParent.insertBefore(getElement(), origSibling);
} else {
hiddenDiv.removeChild(getElement());
}
return child;
}
}