/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.gwt.dom.client;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
/**
* Extends the element implementation provided by GWT to add useful methods. All of them should be removed as soon as
* they make their way into GWT's API.
*
* @version $Id: cf1eab6f989241ffe61ddb7e10251276e6e54d1c $
*/
public class Element extends com.google.gwt.dom.client.Element
{
/**
* The text used in an element's meta data as a place holder for that element's outer HTML.
*/
public static final String INNER_HTML_PLACEHOLDER = "org.xwiki.gwt.dom.client.Element#placeholder";
/**
* The name of the JavaScript property storing the reference to the meta data.
* <p>
* NOTE: We can't use the same name as for {@link #META_DATA_ATTR} because IE stores attribute values as JavaScript
* properties of DOM element objects.
*/
public static final String META_DATA_REF = "metaDataRef";
/**
* The name of the DOM attribute storing the HTML of the meta data. This HTML is used to recreate the meta data when
* an element is cloned or copy and pasted.
*/
public static final String META_DATA_ATTR = "metadata";
/**
* Default constructor. Needs to be protected because all instances are created from JavaScript.
*/
protected Element()
{
super();
}
/**
* Casts a {@link Node} to an instance of this type.
*
* @param node the instance to be casted to this type.
* @return the given object as an instance of {@link Element}.
*/
public static Element as(Node node)
{
return (Element) com.google.gwt.dom.client.Element.as(node);
}
/**
* See http://code.google.com/p/google-web-toolkit/issues/detail?id=3054.
*
* @return The names of DOM attributes present on this element.
*/
public final JsArrayString getAttributeNames()
{
return DOMUtils.getInstance().getAttributeNames(this);
}
/**
* Returns the value of the specified CSS property for this element as it is computed by the browser before the
* element is displayed. The CSS property doesn't have to be applied explicitly or directly on this element. It can
* be inherited or assumed by default on this element.
* <p>
* NOTE: You have to pass the JavaScript name of the property and not its CSS name. The JavaScript name has camel
* case style ({@code fontWeight}) and it is used like this {@code object.style.propertyJSName = value}. The CSS
* name has dash style ({@code font-weight}) and it is used like this {@code propertyCSSName: value;}.
*
* @param propertyName the script name of the CSS property whose value is returned.
* @return the computed value of the specified CSS property for this element.
*/
public final String getComputedStyleProperty(String propertyName)
{
return DOMUtils.getInstance().getComputedStyleProperty(this, propertyName);
}
/**
* Set inner HTML in cross browser manner and notify the owner document.
* <p>
* See http://code.google.com/p/google-web-toolkit/issues/detail?id=3146.
*
* @param html the html to set.
* @see DOMUtils#setInnerHTML(Element, String)
*/
public final void xSetInnerHTML(String html)
{
DOMUtils.getInstance().setInnerHTML(this, html);
((Document) getOwnerDocument()).fireInnerHTMLChange(this);
}
/**
* @return the extended inner HTML of this element, which includes meta data.
* @see #getInnerHTML()
*/
public final String xGetInnerHTML()
{
return Element.as(cloneNode(true)).expandInnerMetaData().getInnerHTML();
}
/**
* @return the extended outer HTML of this element, which includes meta data.
* @see #getString()
*/
public final String xGetString()
{
Node result = Element.as(cloneNode(true)).expandMetaData(true);
return Element.is(result) ? Element.as(result).getString() : DocumentFragment.as(result).getInnerHTML();
}
/**
* Expands inner elements with meta data.
*
* @return this element
*/
public final Element expandInnerMetaData()
{
// Get all the inner elements with meta data.
NodeList<com.google.gwt.dom.client.Element> elements = getElementsByTagName("*");
List<Element> elementsWithMetaData = new ArrayList<Element>();
for (int i = 0; i < elements.getLength(); i++) {
Element element = (Element) elements.getItem(i);
if (element.xHasAttribute(META_DATA_ATTR)) {
elementsWithMetaData.add(element);
}
}
// Expand meta data. Don't iterate the node list directly because it is live and meta data can contain elements.
for (Element element : elementsWithMetaData) {
// Remove the cached reference to the meta data document fragment because it might be shared by clone nodes.
// We could have cloned the meta data document fragment but this is not reliable with some DOM nodes like
// embedded objects.
element.removeProperty(META_DATA_REF);
element.expandMetaData(false);
}
return this;
}
/**
* Expands the meta data of this element and its descendants.
*
* @param deep {@code true} to expand the inner elements with meta data, {@code false} otherwise
* @return this element if it isn't replaced by its meta data, otherwise the document fragment resulted from
* expanding the meta data
*/
public final Node expandMetaData(boolean deep)
{
DocumentFragment metaData = getMetaData();
if (metaData == null) {
return deep ? expandInnerMetaData() : this;
}
// Remove the meta data from the element.
setMetaData(null);
// We have to find the place holder inside the meta data, replace it with this element and then insert the meta
// data where this element was previously located.
// Let's find the place holder.
Iterator<Node> iterator = ((Document) getOwnerDocument()).getIterator(metaData);
while (iterator.hasNext()) {
Node node = iterator.next();
if (INNER_HTML_PLACEHOLDER.equals(node.getNodeValue())) {
// Save the position of this element.
Node hook = ((Document) getOwnerDocument()).createComment("");
if (getParentNode() != null) {
getParentNode().replaceChild(hook, this);
}
// Replace the place holder with this element.
node.getParentNode().replaceChild(this, node);
// Insert the meta data at the right location.
if (hook.getParentNode() != null) {
hook.getParentNode().replaceChild(metaData, hook);
}
if (deep) {
expandInnerMetaData();
}
return metaData;
}
}
// We didn't find the place holder so the meta data will just replace this element.
if (getParentNode() != null) {
getParentNode().replaceChild(metaData, this);
}
return metaData;
}
/**
* Places all the children of this element in a document fragment and returns it.
* <p>
* NOTE: The element will remain empty after this method call.
*
* @return A document fragment containing all the descendants of this element.
*/
public final DocumentFragment extractContents()
{
DocumentFragment contents = ((Document) getOwnerDocument()).createDocumentFragment();
Node child = getFirstChild();
while (child != null) {
contents.appendChild(child);
child = getFirstChild();
}
return contents;
}
/**
* Replaces this element with its child nodes. In other words, all the child nodes of this element are moved to its
* parent node and the element is removed from its parent.
*/
public final void unwrap()
{
if (getParentNode() == null || getParentNode().getNodeType() == Node.DOCUMENT_NODE) {
return;
}
getParentNode().replaceChild(extractContents(), this);
}
/**
* Wraps the passed node and takes its place in its parent. In other words, it adds the passed element as a child of
* this element and replaces it in its parent.
*
* @param node the node to wrap
*/
public final void wrap(Node node)
{
if (node.getParentNode() == null) {
return;
}
node.getParentNode().replaceChild(this, node);
appendChild(node);
}
/**
* @return the meta data associated with this element.
*/
public final DocumentFragment getMetaData()
{
DocumentFragment metaData = (DocumentFragment) getPropertyObject(META_DATA_REF);
// We check the node type because the previous cast has no effect in JavaScript.
if (metaData == null || metaData.getNodeType() != DOMUtils.DOCUMENT_FRAGMENT_NODE) {
// There's no saved reference to the meta data.
// Test if this element has stored meta data.
if (xHasAttribute(META_DATA_ATTR)) {
// This element could be the result of node cloning or copy&paste.
// Let's update the cached meta data reference.
Element container = Element.as(getOwnerDocument().createDivElement());
// Set the inner HTML without notifying the listeners to prevent the meta data from being altered.
DOMUtils.getInstance().setInnerHTML(container, getAttribute(META_DATA_ATTR));
metaData = container.extractContents();
setPropertyObject(META_DATA_REF, metaData);
}
}
return metaData;
};
/**
* Sets the meta data of this element.
*
* @param metaData a document fragment with additional information regarding this element.
*/
public final void setMetaData(DocumentFragment metaData)
{
if (metaData != null) {
// Save a reference to the meta data for fast retrieval.
setPropertyObject(META_DATA_REF, metaData);
// We have to serialize the meta data and store it using a custom attribute to avoid loosing the meta data
// over node cloning or copy&paste. The custom attribute used for storing the meta data should be filtered
// when getting the outer HTML.
setAttribute(META_DATA_ATTR, metaData.getInnerHTML());
} else {
removeProperty(META_DATA_REF);
xRemoveAttribute(META_DATA_ATTR);
}
};
/**
* @return {@code true} if HTML Strict DTD specifies that this element can have children, {@code false} otherwise
*/
public final boolean canHaveChildren()
{
return DOMUtils.getInstance().canHaveChildren(this);
}
/**
* Get the value for the specified attribute in cross browser manner.
*
* @param name the name of the attribute
* @return the value of the attribute
* @see DOMUtils#getAttribute(Element, String)
*/
public final String xGetAttribute(String name)
{
return DOMUtils.getInstance().getAttribute(this, name);
}
/**
* Sets the value for the specified attribute in a cross browser manner.
*
* @param name the name of the attribute
* @param value the value of the attribute
*/
public final void xSetAttribute(String name, String value)
{
DOMUtils.getInstance().setAttribute(this, name, value);
}
/**
* We need this method because {@link #getInnerText()} includes commented text in the output.
* <p>
* See http://code.google.com/p/google-web-toolkit/issues/detail?id=3275.
*
* @return the text between the start and end tags of this element
* @see #getInnerText()
*/
public final String xGetInnerText()
{
return DOMUtils.getInstance().getInnerText(this);
}
/**
* @return {@code true} if this element has any attribute, {@code false} otherwise
*/
public final boolean hasAttributes()
{
return DOMUtils.getInstance().hasAttributes(this);
}
/**
* Ensures this element can be edited in design mode. This method is required because in some browsers you can't
* place the caret inside elements that don't have any visible content and thus you cannot edit them.
*/
public final void ensureEditable()
{
DOMUtils domUtils = DOMUtils.getInstance();
if (domUtils.isInline(this) || getOffsetWidth() == 0) {
return;
}
boolean editable = false;
Node child = getFirstChild();
while (child != null) {
if (child.getNodeType() == Node.TEXT_NODE) {
editable = editable || child.getNodeValue().length() > 0;
} else if (child.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) child;
editable = editable || element.getOffsetWidth() > 0 || domUtils.isOrContainsLineBreak(child);
element.ensureEditable();
}
child = child.getNextSibling();
}
if (!editable) {
domUtils.ensureBlockIsEditable(this);
}
}
/**
* Removes a property from this element.
* <p>
* NOTE: Dynamic properties (expandos) can't be removed from a DOM node in IE 6 and 7. Setting their value to
* {@code null} or {@code undefined} makes them appear in the HTML serialization as attributes. Removing the
* corresponding attribute fails in IE7 if the property value is shared between multiple elements, which can happen
* if elements are cloned. The only solution we've found is to set the property to an empty JavaScript object in IE.
* You should test if the value returned by {@link #getPropertyObject(String)} or {@link #getPropertyJSO(String)} is
* not {@code null} and also if it matches your expected type.
*
* @param propertyName the name of the property to be removed
* @see #setPropertyBoolean(String, boolean)
* @see #setPropertyDouble(String, double)
* @see #setPropertyInt(String, int)
* @see #setPropertyString(String, String)
*/
public final void removeProperty(String propertyName)
{
DOMUtils.getInstance().removeProperty(this, propertyName);
}
/**
* Checks if this element has the specified attribute.
* <p>
* NOTE: We added this method in order to fix an IE7 bug in {@link #removeAttribute(String)}. It seems that
* {@link #cloneNode(boolean)} doesn't clone the attributes in IE7 but only copies their references to the clone. As
* a consequence an attribute can be shared by multiple elements. When we {@link #removeAttribute(String)} the
* {@code specified} flag is set to {@code false} and thus {@link #hasAttribute(String)}, which uses this flag in
* its IE7 implementation, mistakenly reports the attribute as missing from the rest of the elements that share it.
* <p>
* See http://code.google.com/p/google-web-toolkit/issues/detail?id=4690.
*
* @param attributeName the name of an attribute
* @return {@code true} if this element has the specified attribute, {@code false} otherwise
* @see #hasAttribute(String)
*/
public final boolean xHasAttribute(String attributeName)
{
return DOMUtils.getInstance().hasAttribute(this, attributeName);
}
/**
* @param attributeName the name of an attribute
* @return the DOM node associated with the specified attribute
*/
public final native Attribute getAttributeNode(String attributeName)
/*-{
return this.getAttributeNode(attributeName);
}-*/;
/**
* Removes an attribute by name.
* <p>
* We added this method to fix a bug in IE7 which allows <em>shared</em> attribute nodes. Removing a <em>shared</em>
* attribute affects all the element that share it and also can crash the browser if the attribute is remove twice.
*
* @param attributeName the name of the attribute to remove
* @see #xHasAttribute(String)
*/
public final void xRemoveAttribute(String attributeName)
{
DOMUtils.getInstance().removeAttribute(this, attributeName);
}
}