/*
* Copyright (c) 2002-2010 Gargoyle Software 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.gargoylesoftware.htmlunit.javascript.host.html;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;
import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.NativeArray;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequestSettings;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.html.DomAttr;
import com.gargoylesoftware.htmlunit.html.DomCharacterData;
import com.gargoylesoftware.htmlunit.html.DomComment;
import com.gargoylesoftware.htmlunit.html.DomDocumentFragment;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNode;
import com.gargoylesoftware.htmlunit.html.DomText;
import com.gargoylesoftware.htmlunit.html.HTMLParser;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlBody;
import com.gargoylesoftware.htmlunit.html.HtmlDivision;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlFrameSet;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlTable;
import com.gargoylesoftware.htmlunit.html.HtmlTableDataCell;
import com.gargoylesoftware.htmlunit.javascript.NamedNodeMap;
import com.gargoylesoftware.htmlunit.javascript.ScriptableWithFallbackGetter;
import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJob;
import com.gargoylesoftware.htmlunit.javascript.host.Attr;
import com.gargoylesoftware.htmlunit.javascript.host.BoxObject;
import com.gargoylesoftware.htmlunit.javascript.host.Element;
import com.gargoylesoftware.htmlunit.javascript.host.Event;
import com.gargoylesoftware.htmlunit.javascript.host.EventHandler;
import com.gargoylesoftware.htmlunit.javascript.host.MouseEvent;
import com.gargoylesoftware.htmlunit.javascript.host.Node;
import com.gargoylesoftware.htmlunit.javascript.host.TextRange;
import com.gargoylesoftware.htmlunit.javascript.host.TextRectangle;
import com.gargoylesoftware.htmlunit.javascript.host.Window;
import com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleDeclaration;
import com.gargoylesoftware.htmlunit.javascript.host.css.ComputedCSSStyleDeclaration;
/**
* The JavaScript object "HTMLElement" which is the base class for all HTML
* objects. This will typically wrap an instance of {@link HtmlElement}.
*
* @version $Revision: 5420 $
* @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
* @author David K. Taylor
* @author Barnaby Court
* @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
* @author Chris Erskine
* @author David D. Kilzer
* @author Daniel Gredler
* @author Marc Guillemot
* @author Hans Donner
* @author Bruce Faulkner
* @author Ahmed Ashour
* @author Sudhan Moghe
*/
public class HTMLElement extends Element implements ScriptableWithFallbackGetter {
private static final long serialVersionUID = -6864034414262085851L;
private static final Log LOG = LogFactory.getLog(HTMLElement.class);
private static final int BEHAVIOR_ID_UNKNOWN = -1;
/** BEHAVIOR_ID_CLIENT_CAPS. */
public static final int BEHAVIOR_ID_CLIENT_CAPS = 0;
/** BEHAVIOR_ID_HOMEPAGE. */
public static final int BEHAVIOR_ID_HOMEPAGE = 1;
/** BEHAVIOR_ID_DOWNLOAD. */
public static final int BEHAVIOR_ID_DOWNLOAD = 2;
private static final String BEHAVIOR_CLIENT_CAPS = "#default#clientCaps";
private static final String BEHAVIOR_HOMEPAGE = "#default#homePage";
private static final String BEHAVIOR_DOWNLOAD = "#default#download";
static final String POSITION_BEFORE_BEGIN = "beforeBegin";
static final String POSITION_AFTER_BEGIN = "afterBegin";
static final String POSITION_BEFORE_END = "beforeEnd";
static final String POSITION_AFTER_END = "afterEnd";
/**
* Static counter for {@link #uniqueID_}.
*/
private static int UniqueID_Counter_ = 1;
private final Set<String> behaviors_ = new HashSet<String>();
private BoxObject boxObject_; // lazy init
private HTMLCollection all_; // has to be a member to have equality (==) working
private int scrollLeft_;
private int scrollTop_;
private String uniqueID_;
private Map<String, HTMLCollection> elementsByTagName_; // for performance and for equality (==)
private CSSStyleDeclaration style_;
/**
* The value of the "ch" JavaScript attribute for browsers that say that they support it, but do not really
* provide access to the value of the "char" DOM attribute. Not applicable to all types of HTML elements.
*/
private String ch_ = "";
/**
* The value of the "chOff" JavaScript attribute for browsers that say that they support it, but do not really
* provide access to the value of the "charOff" DOM attribute. Not applicable to all types of HTML elements.
*/
private String chOff_ = "";
/**
* The tag names of the objects for which innerHTML is read only in IE
*/
private static final List<String> INNER_HTML_READONLY_IN_IE =
Arrays.asList(new String[] {
"col", "colgroup", "frameset", "head", "html", "style", "table",
"tbody", "tfoot", "thead", "title", "tr"});
/**
* The tag names of the objects for which innerText is read only
*/
private static final List<String> INNER_TEXT_READONLY =
Arrays.asList(new String[] {"html", "table", "tbody", "tfoot", "thead", "tr"});
/**
* The tag names of the objects for which outerHTML is read only
*/
private static final List<String> OUTER_HTML_READONLY =
Arrays.asList(new String[] {
"caption", "col", "colgroup", "frameset", "html",
"tbody", "td", "tfoot", "th", "thead", "tr"});
/**
* Creates an instance.
*/
public HTMLElement() {
elementsByTagName_ = new HashMap<String, HTMLCollection>();
}
/**
* Returns the value of the "all" property.
* @return the value of the "all" property
*/
public HTMLCollection jsxGet_all() {
if (all_ == null) {
all_ = new HTMLCollection(this);
all_.init(getDomNodeOrDie(), ".//*");
}
return all_;
}
/**
* Returns the style object for this element.
* @return the style object for this element
*/
public CSSStyleDeclaration jsxGet_style() {
return style_;
}
/**
* Returns the current (calculated) style object for this element.
* @return the current (calculated) style object for this element
*/
public ComputedCSSStyleDeclaration jsxGet_currentStyle() {
return getWindow().jsxFunction_getComputedStyle(this, null);
}
/**
* Callback method which allows different HTML element types to perform custom
* initialization of computed styles. For example, body elements in most browsers
* have default values for their margins.
*
* @param style the style to initialize
*/
public void setDefaults(final ComputedCSSStyleDeclaration style) {
// Empty by default; override as necessary.
}
/**
* Returns the runtime style object for this element.
* @return the runtime style object for this element
*/
public CSSStyleDeclaration jsxGet_runtimeStyle() {
return style_;
}
/**
* Sets the DOM node that corresponds to this JavaScript object.
* @param domNode the DOM node
*/
@Override
public void setDomNode(final DomNode domNode) {
super.setDomNode(domNode);
style_ = new CSSStyleDeclaration(this);
/**
* Convert JavaScript snippets defined in the attribute map to executable event handlers.
* Should be called only on construction.
*/
final HtmlElement htmlElt = (HtmlElement) domNode;
for (final DomAttr attr : htmlElt.getAttributesMap().values()) {
final String eventName = attr.getName();
if (eventName.startsWith("on")) {
createEventHandler(eventName, attr.getValue());
}
}
}
/**
* Create the event handler function from the attribute value.
* @param eventName the event name (ex: "onclick")
* @param attrValue the attribute value
*/
protected void createEventHandler(final String eventName, final String attrValue) {
final HtmlElement htmlElt = getDomNodeOrDie();
// TODO: check that it is an "allowed" event for the browser, and take care to the case
final BaseFunction eventHandler = new EventHandler(htmlElt, eventName, attrValue);
setEventHandler(eventName, eventHandler);
// forward onload, onclick, ondblclick, ... to window
if ((htmlElt instanceof HtmlBody || htmlElt instanceof HtmlFrameSet)) {
getWindow().getEventListenersContainer()
.setEventHandlerProp(eventName.substring(2), eventHandler);
}
}
/**
* Returns the element ID.
* @return the ID of this element
*/
public String jsxGet_id() {
return getDomNodeOrDie().getId();
}
/**
* Sets the identifier this element.
* @param newId the new identifier of this element
*/
public void jsxSet_id(final String newId) {
getDomNodeOrDie().setId(newId);
}
/**
* Returns the element title.
* @return the ID of this element
*/
public String jsxGet_title() {
return getDomNodeOrDie().getAttribute("title");
}
/**
* Sets the title of this element.
* @param newTitle the new identifier of this element
*/
public void jsxSet_title(final String newTitle) {
getDomNodeOrDie().setAttribute("title", newTitle);
}
/**
* Returns true if this element is disabled.
* @return true if this element is disabled
*/
public boolean jsxGet_disabled() {
return getDomNodeOrDie().hasAttribute("disabled");
}
/**
* Returns the document.
* @return the document
*/
public HTMLDocument jsxGet_document() {
return getWindow().jsxGet_document();
}
/**
* Sets whether or not to disable this element.
* @param disabled True if this is to be disabled
*/
public void jsxSet_disabled(final boolean disabled) {
final HtmlElement element = getDomNodeOrDie();
if (disabled) {
element.setAttribute("disabled", "disabled");
}
else {
element.removeAttribute("disabled");
}
}
/**
* {@inheritDoc}
*/
@Override
public String jsxGet_namespaceURI() {
if (getBrowserVersion().isIE()) {
return getDomNodeOrDie().getNamespaceURI();
}
if (getDomNodeOrDie().getPage() instanceof HtmlPage) {
return null;
}
return getDomNodeOrDie().getNamespaceURI();
}
/**
* {@inheritDoc}
*/
@Override
public String jsxGet_localName() {
final DomNode domNode = getDomNodeOrDie();
String localName = domNode.getLocalName();
if (domNode.getPage() instanceof HtmlPage) {
if (domNode.getPrefix() != null) {
localName = domNode.getPrefix() + ':' + localName;
}
return localName.toUpperCase();
}
return localName;
}
/**
* Looks at attributes with the given name.
* {@inheritDoc}
*/
public Object getWithFallback(final String name) {
if (!name.equals("class")) {
final HtmlElement htmlElement = getDomNodeOrNull();
if (htmlElement != null && isAttributeName(name)) {
final String value = htmlElement.getAttribute(name);
if (HtmlElement.ATTRIBUTE_NOT_DEFINED != value) {
LOG.debug("Found attribute for evaluation of property \"" + name
+ "\" for of " + this);
return value;
}
}
}
return NOT_FOUND;
}
/**
* Indicates if this is the name of a well defined attribute that can be access as property.
* Ex: for HtmlInputElement maxlength => false but maxLength => true
* @param name the name (case sensitive!)
* @return <code>false</code> if no standard attribute exists with this name
*/
protected boolean isAttributeName(final String name) {
// can name be an attribute of current element?
// first approximation: attribute are all lowercase
// this should be improved because it's wrong. For instance: tabIndex, hideFocus, acceptCharset
return name.toLowerCase().equals(name);
}
/**
* For IE, foo.getAttribute(x) uses same names as foo.x.
* @param attributeName the name
* @return the real name
*/
@Override
protected String fixAttributeName(final String attributeName) {
if (getBrowserVersion().isIE()) {
if ("className".equals(attributeName)) {
return "class";
}
else if ("class".equals(attributeName)) {
return "_class"; // attribute should not be retrieved with "class"
}
}
return attributeName;
}
/**
* An IE-only method which clears all custom attributes.
*/
public void jsxFunction_clearAttributes() {
final HtmlElement node = getDomNodeOrDie();
// Remove custom attributes defined directly in HTML.
final List<String> removals = new ArrayList<String>();
for (final String attributeName : node.getAttributesMap().keySet()) {
// Quick hack to figure out what's a "custom" attribute, and what isn't.
// May not be 100% correct.
if (!ScriptableObject.hasProperty(getPrototype(), attributeName)) {
removals.add(attributeName);
}
}
for (final String attributeName : removals) {
node.removeAttribute(attributeName);
}
// Remove custom attributes defined at runtime via JavaScript.
for (final Object id : this.getAllIds()) {
if (id instanceof Integer) {
delete((Integer) id);
}
else if (id instanceof String) {
delete((String) id);
}
}
}
/**
* An IE-only method which copies all custom attributes from the specified source element
* to this element.
* @param source the source element from which to copy the custom attributes
* @param preserveIdentity if <tt>false</tt>, the <tt>name</tt> and <tt>id</tt> attributes are not copied
*/
public void jsxFunction_mergeAttributes(final HTMLElement source, final Object preserveIdentity) {
final HtmlElement src = source.getDomNodeOrDie();
final HtmlElement target = getDomNodeOrDie();
// Merge custom attributes defined directly in HTML.
for (final Map.Entry<String, DomAttr> attribute : src.getAttributesMap().entrySet()) {
// Quick hack to figure out what's a "custom" attribute, and what isn't.
// May not be 100% correct.
final String attributeName = attribute.getKey();
if (!ScriptableObject.hasProperty(getPrototype(), attributeName)) {
final String attributeValue = attribute.getValue().getNodeValue();
target.setAttribute(attributeName, attributeValue);
}
}
// Merge custom attributes defined at runtime via JavaScript.
for (final Object id : source.getAllIds()) {
if (id instanceof Integer) {
final Integer i = (Integer) id;
put(i, this, source.get(i, source));
}
else if (id instanceof String) {
final String s = (String) id;
put(s, this, source.get(s, source));
}
}
// Merge ID and name if we aren't preserving identity.
if (preserveIdentity instanceof Boolean && ((Boolean) preserveIdentity) == false) {
target.setId(src.getId());
target.setAttribute("name", src.getAttribute("name"));
}
}
/**
* Returns the specified attribute.
* @param namespaceURI the namespace URI
* @param localName the local name of the attribute to look for
* @return the specified attribute, <code>null</code> if the attribute is not defined
*/
public Object jsxFunction_getAttributeNodeNS(final String namespaceURI, final String localName) {
return getDomNodeOrDie().getAttributeNodeNS(namespaceURI, localName).getScriptObject();
}
/**
* Gets the specified attribute.
* @param namespaceURI the namespace URI
* @param localName the local name of the attribute to look for
* @return the value of the specified attribute, <code>null</code> if the attribute is not defined
*/
public String jsxFunction_getAttributeNS(final String namespaceURI, final String localName) {
return getDomNodeOrDie().getAttributeNS(namespaceURI, localName);
}
/**
* Test for attribute.
* See also <a href="http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-ElHasAttrNS">
* the DOM reference</a>
*
* @param namespaceURI the namespace URI
* @param localName the local name of the attribute to look for
* @return <code>true</code> if the node has this attribute
*/
public boolean jsxFunction_hasAttributeNS(final String namespaceURI, final String localName) {
return getDomNodeOrDie().hasAttributeNS(namespaceURI, localName);
}
/**
* Sets an attribute.
* See also <a href="http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-F68F082">
* the DOM reference</a>
*
* @param name Name of the attribute to set
* @param value Value to set the attribute to
*/
@Override
public void jsxFunction_setAttribute(String name, final String value) {
name = fixAttributeName(name);
getDomNodeOrDie().setAttribute(name, value);
//FF: call corresponding event handler jsxSet_onxxx if found
if (getBrowserVersion().isFirefox()) {
try {
final Method method = getClass().getMethod("jsxSet_" + name, new Class[] {Object.class});
final String source = "function(){" + value + "}";
method.invoke(this, new Object[] {
Context.getCurrentContext().compileFunction(getWindow(), source, "", 0, null)});
}
catch (final NoSuchMethodException e) {
//silently ignore
}
catch (final IllegalAccessException e) {
//silently ignore
}
catch (final InvocationTargetException e) {
throw new RuntimeException(e.getCause());
}
}
}
/**
* Sets the specified attribute.
* @param namespaceURI the namespace URI
* @param qualifiedName the qualified name of the attribute to look for
* @param value the new attribute value
*/
public void jsxFunction_setAttributeNS(final String namespaceURI, final String qualifiedName, final String value) {
getDomNodeOrDie().setAttributeNS(namespaceURI, qualifiedName, value);
}
/**
* Removes the specified attribute.
* @param namespaceURI the namespace URI of the attribute to remove
* @param localName the local name of the attribute to remove
*/
public void jsxFunction_removeAttributeNS(final String namespaceURI, final String localName) {
getDomNodeOrDie().removeAttributeNS(namespaceURI, localName);
}
/**
* Removes this object from the document hierarchy.
* @param removeChildren whether to remove children or no
* @return a reference to the object that is removed
*/
public HTMLElement jsxFunction_removeNode(final boolean removeChildren) {
final HTMLElement parent = jsxGet_parentElement();
if (parent != null) {
parent.jsxFunction_removeChild(this);
if (!removeChildren) {
final HTMLCollection collection = jsxGet_childNodes();
final int length = collection.jsxGet_length();
for (int i = 0; i < length; i++) {
final Node object = (Node) collection.jsxFunction_item(0);
parent.jsxFunction_appendChild(object);
}
}
}
return this;
}
/**
* Gets the attribute node for the specified attribute.
* @param attributeName the name of the attribute to retrieve
* @return the attribute node for the specified attribute
*/
@Override
public Object jsxFunction_getAttributeNode(final String attributeName) {
return ((NamedNodeMap) jsxGet_attributes()).jsxFunction_getNamedItem(attributeName);
}
/**
* Sets the attribute node for the specified attribute.
* @param newAtt the attribute to set
* @return the replaced attribute node, if any
*/
public Attr jsxFunction_setAttributeNode(final Attr newAtt) {
final String name = newAtt.jsxGet_name();
final String value = newAtt.jsxGet_value();
final Attr replacedAtt = (Attr) jsxFunction_getAttributeNode(name);
if (replacedAtt != null) {
replacedAtt.detachFromParent();
}
getDomNodeOrDie().setAttribute(name, value);
return replacedAtt;
}
/**
* Returns all the descendant elements with the specified tag name.
* @param tagName the name to search for
* @return all the descendant elements with the specified tag name
*/
@Override
public Object jsxFunction_getElementsByTagName(String tagName) {
tagName = tagName.toLowerCase();
HTMLCollection collection = elementsByTagName_.get(tagName);
if (collection != null) {
return collection;
}
final DomNode node = getDomNodeOrDie();
collection = new HTMLCollection(this);
final String xpath;
if ("*".equals(tagName)) {
xpath = ".//*";
}
else {
xpath = ".//*[local-name() = '" + tagName + "']";
}
collection.init(node, xpath);
elementsByTagName_.put(tagName, collection);
return collection;
}
/**
* Returns all the descendant elements with the specified class.
* @param className the name to search for
* @return all the descendant elements with the specified class name
*/
public HTMLCollection jsxFunction_getElementsByClassName(final String className) {
final HTMLCollection collection = new HTMLCollection(this);
final String[] classNames = className.split("\\s");
final StringBuilder exp = new StringBuilder();
for (final String name : classNames) {
if (exp.length() != 0) {
exp.append(" and ");
}
exp.append("contains(concat(' ', @class, ' '), ' ");
exp.append(name);
exp.append(" ')");
}
exp.insert(0, ".//*[");
exp.append("]");
collection.init(getDomNodeOrDie(), exp.toString());
return collection;
}
/**
* Returns the class defined for this element.
* @return the class name
*/
public Object jsxGet_className() {
return getDomNodeOrDie().getAttribute("class");
}
/**
* Returns "clientHeight" attribute.
* @return the "clientHeight" attribute
*/
public int jsxGet_clientHeight() {
final boolean includePadding = !getBrowserVersion().isIE();
final ComputedCSSStyleDeclaration style = getWindow().jsxFunction_getComputedStyle(this, null);
return style.getCalculatedHeight(false, includePadding);
}
/**
* Returns "clientWidth" attribute.
* @return the "clientWidth" attribute
*/
public int jsxGet_clientWidth() {
final boolean includePadding = !getBrowserVersion().isIE();
final ComputedCSSStyleDeclaration style = getWindow().jsxFunction_getComputedStyle(this, null);
return style.getCalculatedWidth(false, includePadding);
}
/**
* Returns "clientLeft" attribute.
* @return the "clientLeft" attribute
*/
public int jsxGet_clientLeft() {
return 2; // TODO!!!
}
/**
* Returns "clientTop" attribute.
* @return the "clientTop" attribute
*/
public int jsxGet_clientTop() {
return 2; // TODO!!!
}
/**
* Sets the class attribute for this element.
* @param className the new class name
*/
public void jsxSet_className(final String className) {
getDomNodeOrDie().setAttribute("class", className);
}
/**
* Gets the innerHTML attribute.
* @return the contents of this node as HTML
*/
public String jsxGet_innerHTML() {
final StringBuilder buf = new StringBuilder();
// we can't rely on DomNode.asXml because it adds indentation and new lines
printChildren(buf, getDomNodeOrDie(), !"SCRIPT".equals(jsxGet_tagName()));
return buf.toString();
}
/**
* Gets the innerText attribute.
* @return the contents of this node as text
*/
public String jsxGet_innerText() {
final StringBuilder buf = new StringBuilder();
// we can't rely on DomNode.asXml because it adds indentation and new lines
printChildren(buf, getDomNodeOrDie(), false);
return buf.toString();
}
/**
* Gets the outerHTML of the node.
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534310.aspx">MSDN documentation</a>
* @return the contents of this node as HTML
*/
public String jsxGet_outerHTML() {
final StringBuilder buf = new StringBuilder();
// we can't rely on DomNode.asXml because it adds indentation and new lines
printNode(buf, getDomNodeOrDie(), true);
return buf.toString();
}
private void printChildren(final StringBuilder buffer, final DomNode node, final boolean html) {
for (final DomNode child : node.getChildren()) {
printNode(buffer, child, html);
}
}
private void printNode(final StringBuilder buffer, final DomNode node, final boolean html) {
if (node instanceof DomComment) {
// Remove whitespace sequences.
final String s = node.getNodeValue().replaceAll(" ", " ");
buffer.append("<!--").append(s).append("-->");
}
else if (node instanceof DomCharacterData) {
// Remove whitespace sequences, possibly escape XML characters.
String s = node.getNodeValue().replaceAll(" ", " ");
if (html) {
s = com.gargoylesoftware.htmlunit.util.StringUtils.escapeXmlChars(s);
}
buffer.append(s);
}
else if (html) {
// Start the tag name. IE does it in uppercase, FF in lowercase.
final HtmlElement element = (HtmlElement) node;
final boolean ie = getBrowserVersion().isIE();
String tag = element.getTagName();
if (ie) {
tag = tag.toUpperCase();
}
buffer.append("<").append(tag);
// Add the attributes. IE does not use quotes, FF does.
for (final DomAttr attr : element.getAttributesMap().values()) {
final String name = attr.getName();
final String value = attr.getValue().replaceAll("\"", """);
final boolean quote = !ie
|| com.gargoylesoftware.htmlunit.util.StringUtils.containsWhitespace(value)
|| value.length() == 0
|| (element instanceof HtmlAnchor && "href".equals(name));
buffer.append(' ').append(name).append("=");
if (quote) {
buffer.append("\"");
}
buffer.append(value);
if (quote) {
buffer.append("\"");
}
}
buffer.append(">");
// Add the children.
printChildren(buffer, node, html);
// Close the tag. IE does it in uppercase, FF in lowercase.
buffer.append("</").append(tag).append(">");
}
else {
final HtmlElement element = (HtmlElement) node;
if (element.getTagName().equals("p")) {
buffer.append("\r\n"); // \r\n because it's to implement something IE specific
}
if (!element.getTagName().equals("script")) {
printChildren(buffer, node, html);
}
}
}
/**
* Replace all children elements of this element with the supplied value.
* @param value the new value for the contents of this node
*/
public void jsxSet_innerHTML(final Object value) {
final DomNode domNode = getDomNodeOrDie();
final boolean ie = getBrowserVersion().isIE();
if (ie && INNER_HTML_READONLY_IN_IE.contains(domNode.getNodeName())) {
throw Context.reportRuntimeError("innerHTML is read-only for tag " + domNode.getNodeName());
}
domNode.removeAllChildren();
// null && IE -> add child
// null && non-IE -> Don't add
// '' -> Don't add
if ((value == null && ie) || (value != null && !"".equals(value))) {
final String valueAsString = Context.toString(value);
parseHtmlSnippet(domNode, true, valueAsString);
//if the parentNode has null parentNode in IE,
//create a DocumentFragment to be the parentNode's parentNode.
if (domNode.getParentNode() == null && ie) {
final DomDocumentFragment fragment = ((HtmlPage) domNode.getPage()).createDomDocumentFragment();
fragment.appendChild(domNode);
}
}
}
/**
* Replace all children elements of this element with the supplied value.
* @param value the new value for the contents of this node
*/
public void jsxSet_innerText(final String value) {
setInnerText(Context.toString(value));
}
private void setInnerText(final String value) {
final DomNode domNode = getDomNodeOrDie();
if (INNER_TEXT_READONLY.contains(domNode.getNodeName())) {
throw Context.reportRuntimeError("innerText is read-only for tag " + domNode.getNodeName());
}
domNode.removeAllChildren();
if (value != null && value.length() != 0) {
domNode.appendChild(new DomText(domNode.getPage(), Context.toString(value)));
}
//if the parentNode has null parentNode in IE,
//create a DocumentFragment to be the parentNode's parentNode.
if (domNode.getParentNode() == null && getBrowserVersion().isIE()) {
final DomDocumentFragment fragment = ((HtmlPage) domNode.getPage()).createDomDocumentFragment();
fragment.appendChild(domNode);
}
}
/**
* Replace all children elements of this element with the supplied value.
* @param value the new value for the contents of this node
*/
@Override
public void jsxSet_textContent(final Object value) {
setInnerText(value == null ? null : Context.toString(value));
}
/**
* Replace all children elements of this element with the supplied value.
* Sets the outerHTML of the node.
* @param value the new value for replacing this node
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534310.aspx">MSDN documentation</a>
*/
public void jsxSet_outerHTML(final String value) {
final DomNode domNode = getDomNodeOrDie();
if (OUTER_HTML_READONLY.contains(domNode.getNodeName())) {
throw Context.reportRuntimeError("outerHTML is read-only for tag " + domNode.getNodeName());
}
final DomNode proxyNode = new ProxyDomNode(domNode.getPage(), domNode, false);
parseHtmlSnippet(proxyNode, false, value);
domNode.remove();
}
/**
* Parses the specified HTML source code, appending the resultant content at the specified target location.
* @param target the node indicating the position at which the parsed content should be placed
* @param append if <tt>true</tt>, append the parsed content as a child of the specified target;
* if <tt>false</tt>, append the parsed content as the previous sibling of the specified target
* @param source the HTML code extract to parse
*/
public static void parseHtmlSnippet(final DomNode target, final boolean append, final String source) {
try {
HTMLParser.parseFragment(target, source);
}
catch (final IOException e) {
LogFactory.getLog(HtmlElement.class).error("Unexpected exception occurred while parsing HTML snippet", e);
throw Context.reportRuntimeError("Unexpected exception occurred while parsing HTML snippet: "
+ e.getMessage());
}
catch (final SAXException e) {
LogFactory.getLog(HtmlElement.class).error("Unexpected exception occurred while parsing HTML snippet", e);
throw Context.reportRuntimeError("Unexpected exception occurred while parsing HTML snippet: "
+ e.getMessage());
}
}
/**
* ProxyDomNode.
*/
public static class ProxyDomNode extends HtmlDivision {
private static final long serialVersionUID = -7816775277187498538L;
private final DomNode target_;
private boolean append_;
/**
* Constructor.
* @param page the page
* @param target the target
* @param append append or no
*/
public ProxyDomNode(final SgmlPage page, final DomNode target, final boolean append) {
super(null, HtmlDivision.TAG_NAME, page, null);
target_ = target;
append_ = append;
}
/**
* {@inheritDoc}
*/
@Override
public DomNode appendChild(final org.w3c.dom.Node node) {
final DomNode domNode = (DomNode) node;
if (append_) {
return target_.appendChild(domNode);
}
target_.insertBefore(domNode);
return domNode;
}
/**
* Gets wrapped DomNode.
* @return the node
*/
public DomNode getDomNode() {
return target_;
}
/**
* Returns append or not.
* @return append or not
*/
public boolean isAppend() {
return append_;
}
}
/**
* Gets the attributes of the element in the form of a {@link org.xml.sax.Attributes}.
* @param element the element to read the attributes from
* @return the attributes
*/
protected AttributesImpl readAttributes(final HtmlElement element) {
final AttributesImpl attributes = new AttributesImpl();
for (final DomAttr entry : element.getAttributesMap().values()) {
final String name = entry.getName();
final String value = entry.getValue();
attributes.addAttribute(null, name, name, null, value);
}
return attributes;
}
/**
* Inserts the given HTML text into the element at the location.
* @see <a href="http://msdn2.microsoft.com/en-us/library/ms536452.aspx">
* MSDN documentation</a>
* @param where specifies where to insert the HTML text, using one of the following value:
* beforeBegin, afterBegin, beforeEnd, afterEnd
* @param text the HTML text to insert
*/
public void jsxFunction_insertAdjacentHTML(final String where, final String text) {
final Object[] values = getInsertAdjacentLocation(where);
final DomNode node = (DomNode) values[0];
final boolean append = ((Boolean) values[1]).booleanValue();
// add the new nodes
final DomNode proxyNode = new ProxyDomNode(node.getPage(), node, append);
parseHtmlSnippet(proxyNode, append, text);
}
/**
* Inserts the given element into the element at the location.
* @see <a href="http://msdn2.microsoft.com/en-us/library/ms536451.aspx">
* MSDN documentation</a>
* @param where specifies where to insert the element, using one of the following value:
* beforeBegin, afterBegin, beforeEnd, afterEnd
* @param object the element to insert
* @return an element object
*/
public Object jsxFunction_insertAdjacentElement(final String where, final Object object) {
if (object instanceof Node) {
final DomNode childNode = ((Node) object).getDomNodeOrDie();
final Object[] values = getInsertAdjacentLocation(where);
final DomNode node = (DomNode) values[0];
final boolean append = ((Boolean) values[1]).booleanValue();
if (append) {
node.appendChild(childNode);
}
else {
node.insertBefore(childNode);
}
return object;
}
throw Context.reportRuntimeError("Passed object is not an element: " + object);
}
/**
* Returns where and how to add the new node.
* Used by {@link #jsxFunction_insertAdjacentHTML(String, String)} and
* {@link #jsxFunction_insertAdjacentElement(String, Object)}.
*
* @param where specifies where to insert the element, using one of the following value:
* beforeBegin, afterBegin, beforeEnd, afterEnd
*
* @return an array of 1-DomNode:parentNode and 2-Boolean:append
*/
private Object[] getInsertAdjacentLocation(final String where) {
final DomNode currentNode = getDomNodeOrDie();
final DomNode node;
final boolean append;
// compute the where and how the new nodes should be added
if (POSITION_AFTER_BEGIN.equalsIgnoreCase(where)) {
if (currentNode.getFirstChild() == null) {
// new nodes should appended to the children of current node
node = currentNode;
append = true;
}
else {
// new nodes should be inserted before first child
node = currentNode.getFirstChild();
append = false;
}
}
else if (POSITION_BEFORE_BEGIN.equalsIgnoreCase(where)) {
// new nodes should be inserted before current node
node = currentNode;
append = false;
}
else if (POSITION_BEFORE_END.equalsIgnoreCase(where)) {
// new nodes should appended to the children of current node
node = currentNode;
append = true;
}
else if (POSITION_AFTER_END.equalsIgnoreCase(where)) {
if (currentNode.getNextSibling() == null) {
// new nodes should appended to the children of parent node
node = currentNode.getParentNode();
append = true;
}
else {
// new nodes should be inserted before current node's next sibling
node = currentNode.getNextSibling();
append = false;
}
}
else {
throw Context.reportRuntimeError("Illegal position value: \"" + where + "\"");
}
if (append) {
return new Object[] {node, Boolean.TRUE};
}
return new Object[] {node, Boolean.FALSE};
}
/**
* Adds the specified behavior to this HTML element. Currently only supports
* the following default IE behaviors:
* <ul>
* <li>#default#clientCaps</li>
* <li>#default#homePage</li>
* <li>#default#download</li>
* </ul>
* @param behavior the URL of the behavior to add, or a default behavior name
* @return an identifier that can be user later to detach the behavior from the element
*/
public int jsxFunction_addBehavior(final String behavior) {
// if behavior already defined, then nothing to do
if (behaviors_.contains(behavior)) {
return 0;
}
final Class< ? extends HTMLElement> c = getClass();
if (BEHAVIOR_CLIENT_CAPS.equalsIgnoreCase(behavior)) {
defineProperty("availHeight", c, 0);
defineProperty("availWidth", c, 0);
defineProperty("bufferDepth", c, 0);
defineProperty("colorDepth", c, 0);
defineProperty("connectionType", c, 0);
defineProperty("cookieEnabled", c, 0);
defineProperty("cpuClass", c, 0);
defineProperty("height", c, 0);
defineProperty("javaEnabled", c, 0);
defineProperty("platform", c, 0);
defineProperty("systemLanguage", c, 0);
defineProperty("userLanguage", c, 0);
defineProperty("width", c, 0);
defineFunctionProperties(new String[] {"addComponentRequest"}, c, 0);
defineFunctionProperties(new String[] {"clearComponentRequest"}, c, 0);
defineFunctionProperties(new String[] {"compareVersions"}, c, 0);
defineFunctionProperties(new String[] {"doComponentRequest"}, c, 0);
defineFunctionProperties(new String[] {"getComponentVersion"}, c, 0);
defineFunctionProperties(new String[] {"isComponentInstalled"}, c, 0);
behaviors_.add(BEHAVIOR_CLIENT_CAPS);
return BEHAVIOR_ID_CLIENT_CAPS;
}
else if (BEHAVIOR_HOMEPAGE.equalsIgnoreCase(behavior)) {
defineFunctionProperties(new String[] {"isHomePage"}, c, 0);
defineFunctionProperties(new String[] {"setHomePage"}, c, 0);
defineFunctionProperties(new String[] {"navigateHomePage"}, c, 0);
behaviors_.add(BEHAVIOR_CLIENT_CAPS);
return BEHAVIOR_ID_HOMEPAGE;
}
else if (BEHAVIOR_DOWNLOAD.equalsIgnoreCase(behavior)) {
defineFunctionProperties(new String[] {"startDownload"}, c, 0);
behaviors_.add(BEHAVIOR_DOWNLOAD);
return BEHAVIOR_ID_DOWNLOAD;
}
else {
LOG.warn("Unimplemented behavior: " + behavior);
return BEHAVIOR_ID_UNKNOWN;
}
}
/**
* Removes the behavior corresponding to the specified identifier from this element.
* @param id the identifier for the behavior to remove
*/
public void jsxFunction_removeBehavior(final int id) {
switch (id) {
case BEHAVIOR_ID_CLIENT_CAPS:
delete("availHeight");
delete("availWidth");
delete("bufferDepth");
delete("colorDepth");
delete("connectionType");
delete("cookieEnabled");
delete("cpuClass");
delete("height");
delete("javaEnabled");
delete("platform");
delete("systemLanguage");
delete("userLanguage");
delete("width");
delete("addComponentRequest");
delete("clearComponentRequest");
delete("compareVersions");
delete("doComponentRequest");
delete("getComponentVersion");
delete("isComponentInstalled");
behaviors_.remove(BEHAVIOR_CLIENT_CAPS);
break;
case BEHAVIOR_ID_HOMEPAGE:
delete("isHomePage");
delete("setHomePage");
delete("navigateHomePage");
behaviors_.remove(BEHAVIOR_HOMEPAGE);
break;
case BEHAVIOR_ID_DOWNLOAD:
delete("startDownload");
behaviors_.remove(BEHAVIOR_DOWNLOAD);
break;
default:
LOG.warn("Unexpected behavior id: " + id + ". Ignoring.");
}
}
//----------------------- START #default#clientCaps BEHAVIOR -----------------------
/**
* Returns the screen's available height. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the screen's available height
*/
public int getAvailHeight() {
return getWindow().jsxGet_screen().jsxGet_availHeight();
}
/**
* Returns the screen's available width. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the screen's available width
*/
public int getAvailWidth() {
return getWindow().jsxGet_screen().jsxGet_availWidth();
}
/**
* Returns the screen's buffer depth. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the screen's buffer depth
*/
public int getBufferDepth() {
return getWindow().jsxGet_screen().jsxGet_bufferDepth();
}
/**
* Returns the BoxObject for this element.
* @return the BoxObject for this element
*/
public BoxObject getBoxObject() {
if (boxObject_ == null) {
boxObject_ = new BoxObject(this);
boxObject_.setParentScope(getWindow());
boxObject_.setPrototype(getPrototype(boxObject_.getClass()));
}
return boxObject_;
}
/**
* Returns the screen's color depth. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the screen's color depth
*/
public int getColorDepth() {
return getWindow().jsxGet_screen().jsxGet_colorDepth();
}
/**
* Returns the connection type being used. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the connection type being used
* Current implementation always return "modem"
*/
public String getConnectionType() {
return "modem";
}
/**
* Returns <tt>true</tt> if cookies are enabled. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return whether or not cookies are enabled
*/
public boolean getCookieEnabled() {
return getWindow().jsxGet_navigator().jsxGet_cookieEnabled();
}
/**
* Returns the type of CPU used. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the type of CPU used
*/
public String getCpuClass() {
return getWindow().jsxGet_navigator().jsxGet_cpuClass();
}
/**
* Returns the screen's height. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the screen's height
*/
public int getHeight() {
return getWindow().jsxGet_screen().jsxGet_height();
}
/**
* Returns <tt>true</tt> if Java is enabled. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return whether or not Java is enabled
*/
public boolean getJavaEnabled() {
return getWindow().jsxGet_navigator().jsxFunction_javaEnabled();
}
/**
* Returns the platform used. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the platform used
*/
public String getPlatform() {
return getWindow().jsxGet_navigator().jsxGet_platform();
}
/**
* Returns the system language. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the system language
*/
public String getSystemLanguage() {
return getWindow().jsxGet_navigator().jsxGet_systemLanguage();
}
/**
* Returns the user language. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the user language
*/
public String getUserLanguage() {
return getWindow().jsxGet_navigator().jsxGet_userLanguage();
}
/**
* Returns the screen's width. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @return the screen's width
*/
public int getWidth() {
return getWindow().jsxGet_screen().jsxGet_width();
}
/**
* Adds the specified component to the queue of components to be installed. Note
* that no components ever get installed, and this call is always ignored. Part of
* the <tt>#default#clientCaps</tt> default IE behavior implementation.
* @param id the identifier for the component to install
* @param idType the type of identifier specified
* @param minVersion the minimum version of the component to install
*/
public void addComponentRequest(final String id, final String idType, final String minVersion) {
LOG.debug("Call to addComponentRequest(" + id + ", " + idType + ", " + minVersion + ") ignored.");
}
/**
* Clears the component install queue of all component requests. Note that no components
* ever get installed, and this call is always ignored. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
*/
public void clearComponentRequest() {
LOG.debug("Call to clearComponentRequest() ignored.");
}
/**
* Compares the two specified version numbers. Part of the <tt>#default#clientCaps</tt>
* default IE behavior implementation.
* @param v1 the first of the two version numbers to compare
* @param v2 the second of the two version numbers to compare
* @return -1 if v1 < v2, 0 if v1 = v2, and 1 if v1 > v2
*/
public int compareVersions(final String v1, final String v2) {
final int i = v1.compareTo(v2);
if (i == 0) { return 0; }
else if (i < 0) { return -1; }
else { return 1; }
}
/**
* Downloads all the components queued via {@link #addComponentRequest(String, String, String)}.
* @return <tt>true</tt> if the components are downloaded successfully
* Current implementation always return <code>false</code>
*/
public boolean doComponentRequest() {
return false;
}
/**
* Returns the version of the specified component.
* @param id the identifier for the component whose version is to be returned
* @param idType the type of identifier specified
* @return the version of the specified component
*/
public String getComponentVersion(final String id, final String idType) {
if ("{E5D12C4E-7B4F-11D3-B5C9-0050045C3C96}".equals(id)) {
// Yahoo Messenger.
return "";
}
// Everything else.
return "1.0";
}
/**
* Returns <tt>true</tt> if the specified component is installed.
* @param id the identifier for the component to check for
* @param idType the type of id specified
* @param minVersion the minimum version to check for
* @return <tt>true</tt> if the specified component is installed
*/
public boolean isComponentInstalled(final String id, final String idType, final String minVersion) {
return false;
}
//----------------------- START #default#download BEHAVIOR -----------------------
/**
* Implementation of the IE behavior #default#download.
* @param uri the URI of the download source
* @param callback the method which should be called when the download is finished
* @see <a href="http://msdn.microsoft.com/en-us/library/ms531406.aspx">MSDN documentation</a>
* @throws MalformedURLException if the URL cannot be created
*/
public void startDownload(final String uri, final Function callback) throws MalformedURLException {
final HtmlPage page = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
final URL url = page.getFullyQualifiedUrl(uri);
if (!page.getWebResponse().getRequestSettings().getUrl().getHost().equals(url.getHost())) {
throw Context.reportRuntimeError("Not authorized url: " + url);
}
final JavaScriptJob job = new DownloadBehaviorJob(url, callback);
page.getEnclosingWindow().getJobManager().addJob(job, page);
}
/**
* A helper class for the IE behavior #default#download
* This represents a download action. The download is handled
* asynchronously, when the download is finished, the method specified
* by callback is called with one argument - the content of the response as string.
* @see #startDownload(String, Function)
* @author <a href="mailto:stefan@anzinger.net">Stefan Anzinger</a>
*/
private final class DownloadBehaviorJob extends JavaScriptJob {
private final URL url_;
private final Function callback_;
/**
* Creates a new instance.
* @param url the URL to download
* @param callback the callback function to call
*/
private DownloadBehaviorJob(final URL url, final Function callback) {
url_ = url;
callback_ = callback;
}
/**
* Performs the download and calls the callback method.
*/
public void run() {
final WebClient client = getWindow().getWebWindow().getWebClient();
final Scriptable scope = callback_.getParentScope();
final WebRequestSettings settings = new WebRequestSettings(url_);
try {
final WebResponse webResponse = client.loadWebResponse(settings);
final String content = webResponse.getContentAsString();
LOG.debug("Downloaded content: " + StringUtils.abbreviate(content, 512));
final Object[] args = new Object[] {content};
final ContextAction action = new ContextAction() {
public Object run(final Context cx) {
callback_.call(cx, scope, scope, args);
return null;
}
};
final ContextFactory cf = client.getJavaScriptEngine().getContextFactory();
cf.call(action);
}
catch (final IOException e) {
LOG.error("Behavior #default#download: Cannot download " + url_, e);
}
}
}
//----------------------- END #default#download BEHAVIOR -----------------------
//----------------------- START #default#homePage BEHAVIOR -----------------------
/**
* Returns <tt>true</tt> if the specified URL is the web client's current
* homepage and the document calling the method is on the same domain as the
* user's homepage. Part of the <tt>#default#homePage</tt> default IE behavior
* implementation.
* @param url the URL to check
* @return <tt>true</tt> if the specified URL is the current homepage
*/
public boolean isHomePage(final String url) {
try {
final URL newUrl = new URL(url);
final URL currentUrl = getDomNodeOrDie().getPage().getWebResponse().getRequestSettings().getUrl();
final String home = getDomNodeOrDie().getPage().getEnclosingWindow().getWebClient().getHomePage();
final boolean sameDomains = newUrl.getHost().equalsIgnoreCase(currentUrl.getHost());
final boolean isHomePage = (home != null && home.equals(url));
return (sameDomains && isHomePage);
}
catch (final MalformedURLException e) {
return false;
}
}
/**
* Sets the web client's current homepage. Part of the <tt>#default#homePage</tt>
* default IE behavior implementation.
* @param url the new homepage URL
*/
public void setHomePage(final String url) {
getDomNodeOrDie().getPage().getEnclosingWindow().getWebClient().setHomePage(url);
}
/**
* Causes the web client to navigate to the current home page. Part of the
* <tt>#default#homePage</tt> default IE behavior implementation.
* @throws IOException if loading home page fails
*/
public void navigateHomePage() throws IOException {
final WebClient webClient = getDomNodeOrDie().getPage().getEnclosingWindow().getWebClient();
webClient.getPage(webClient.getHomePage());
}
//----------------------- END #default#homePage BEHAVIOR -----------------------
/**
* Gets the children of the current node.
* @see <a href="http://msdn.microsoft.com/en-us/library/ms537446.aspx">MSDN documentation</a>
* @return the child at the given position
*/
public HTMLCollection jsxGet_children() {
final HTMLCollection children = new HTMLCollection(this);
children.init(getDomNodeOrDie(), "./*");
return children;
}
/**
* Returns this element's <tt>offsetHeight</tt>, which is the element height plus the element's padding
* plus the element's border. This method returns a dummy value compatible with mouse event coordinates
* during mouse events.
* @return this element's <tt>offsetHeight</tt>
* @see <a href="http://msdn2.microsoft.com/en-us/library/ms534199.aspx">MSDN Documentation</a>
* @see <a href="http://www.quirksmode.org/js/elementdimensions.html">Element Dimensions</a>
*/
public int jsxGet_offsetHeight() {
final MouseEvent event = MouseEvent.getCurrentMouseEvent();
if (isAncestorOfEventTarget(event)) {
// compute appropriate offset height to pretend mouse event was produced within this element
return event.jsxGet_clientY() - getPosY() + 50;
}
return jsxGet_currentStyle().getCalculatedHeight(true, true);
}
/**
* Returns this element's <tt>offsetWidth</tt>, which is the element width plus the element's padding
* plus the element's border. This method returns a dummy value compatible with mouse event coordinates
* during mouse events.
* @return this element's <tt>offsetWidth</tt>
* @see <a href="http://msdn2.microsoft.com/en-us/library/ms534304.aspx">MSDN Documentation</a>
* @see <a href="http://www.quirksmode.org/js/elementdimensions.html">Element Dimensions</a>
*/
public int jsxGet_offsetWidth() {
final MouseEvent event = MouseEvent.getCurrentMouseEvent();
if (isAncestorOfEventTarget(event)) {
// compute appropriate offset width to pretend mouse event was produced within this element
return event.jsxGet_clientX() - getPosX() + 50;
}
return jsxGet_currentStyle().getCalculatedWidth(true, true);
}
/**
* Returns <tt>true</tt> if this element's node is an ancestor of the specified event's target node.
* @param event the event whose target node is to be checked
* @return <tt>true</tt> if this element's node is an ancestor of the specified event's target node
*/
protected boolean isAncestorOfEventTarget(final MouseEvent event) {
if (event == null) {
return false;
}
else if (!(event.jsxGet_srcElement() instanceof HTMLElement)) {
return false;
}
final HTMLElement srcElement = (HTMLElement) event.jsxGet_srcElement();
return getDomNodeOrDie().isAncestorOf(srcElement.getDomNodeOrDie());
}
/**
* Returns this element's X position.
* @return this element's X position
*/
public int getPosX() {
int cumulativeOffset = 0;
HTMLElement element = this;
while (element != null) {
cumulativeOffset += element.jsxGet_offsetLeft();
if (element != this) {
cumulativeOffset += element.jsxGet_currentStyle().getBorderLeft();
}
element = element.getOffsetParent();
}
return cumulativeOffset;
}
/**
* Gets the offset parent or <code>null</code> if this is not an {@link HTMLElement}.
* @return the offset parent or <code>null</code>
*/
private HTMLElement getOffsetParent() {
final Object offsetParent = jsxGet_offsetParent();
if (offsetParent instanceof HTMLElement) {
return (HTMLElement) offsetParent;
}
return null;
}
/**
* Returns this element's Y position.
* @return this element's Y position
*/
public int getPosY() {
int cumulativeOffset = 0;
HTMLElement element = this;
while (element != null) {
cumulativeOffset += element.jsxGet_offsetTop();
if (element != this) {
cumulativeOffset += element.jsxGet_currentStyle().getBorderTop();
}
element = element.getOffsetParent();
}
return cumulativeOffset;
}
/**
* Returns this element's <tt>offsetLeft</tt>, which is the calculated left position of this
* element relative to the <tt>offsetParent</tt>.
*
* @return this element's <tt>offsetLeft</tt>
* @see <a href="http://msdn2.microsoft.com/en-us/library/ms534200.aspx">MSDN Documentation</a>
* @see <a href="http://www.quirksmode.org/js/elementdimensions.html">Element Dimensions</a>
* @see <a href="http://dump.testsuite.org/2006/dom/style/offset/spec">Reverse Engineering by Anne van Kesteren</a>
*/
public int jsxGet_offsetLeft() {
if (this instanceof HTMLBodyElement) {
return 0;
}
int left = 0;
final HTMLElement offsetParent = getOffsetParent();
// Add the offset for this node.
DomNode node = getDomNodeOrDie();
HTMLElement element = (HTMLElement) node.getScriptObject();
left += element.jsxGet_currentStyle().getLeft(true, false, false);
// If this node is absolutely positioned, we're done.
final String position = element.jsxGet_currentStyle().getPositionWithInheritance();
if ("absolute".equals(position)) {
return left;
}
// Add the offset for the ancestor nodes.
node = node.getParentNode();
while (node != null && node.getScriptObject() != offsetParent) {
if (node.getScriptObject() instanceof HTMLElement) {
element = (HTMLElement) node.getScriptObject();
left += element.jsxGet_currentStyle().getLeft(true, true, true);
}
node = node.getParentNode();
}
if (offsetParent != null) {
left += offsetParent.jsxGet_currentStyle().getMarginLeft();
left += offsetParent.jsxGet_currentStyle().getPaddingLeft();
}
return left;
}
/**
* Returns this element's <tt>offsetTop</tt>, which is the calculated top position of this
* element relative to the <tt>offsetParent</tt>.
*
* @return this element's <tt>offsetTop</tt>
* @see <a href="http://msdn2.microsoft.com/en-us/library/ms534303.aspx">MSDN Documentation</a>
* @see <a href="http://www.quirksmode.org/js/elementdimensions.html">Element Dimensions</a>
* @see <a href="http://dump.testsuite.org/2006/dom/style/offset/spec">Reverse Engineering by Anne van Kesteren</a>
*/
public int jsxGet_offsetTop() {
if (this instanceof HTMLBodyElement) {
return 0;
}
int top = 0;
final HTMLElement offsetParent = getOffsetParent();
// Add the offset for this node.
DomNode node = getDomNodeOrDie();
HTMLElement element = (HTMLElement) node.getScriptObject();
top += element.jsxGet_currentStyle().getTop(true, false, false);
// If this node is absolutely positioned, we're done.
final String position = element.jsxGet_currentStyle().getPositionWithInheritance();
if ("absolute".equals(position)) {
return top;
}
// Add the offset for the ancestor nodes.
node = node.getParentNode();
while (node != null && node.getScriptObject() != offsetParent) {
if (node.getScriptObject() instanceof HTMLElement) {
element = (HTMLElement) node.getScriptObject();
top += element.jsxGet_currentStyle().getTop(false, true, true);
}
node = node.getParentNode();
}
if (offsetParent != null) {
final HTMLElement thiz = (HTMLElement) getDomNodeOrDie().getScriptObject();
final boolean thisElementHasTopMargin = (thiz.jsxGet_currentStyle().getMarginTop() != 0);
if (!thisElementHasTopMargin) {
top += offsetParent.jsxGet_currentStyle().getMarginTop();
}
top += offsetParent.jsxGet_currentStyle().getPaddingTop();
}
return top;
}
/**
* Returns this element's <tt>offsetParent</tt>. The <tt>offsetLeft</tt> and
* <tt>offsetTop</tt> attributes are relative to the <tt>offsetParent</tt>.
*
* @return this element's <tt>offsetParent</tt>. This may be <code>undefined</code> when this node is
* not attached or <code>null</code> for <code>body</code>.
* @see <a href="http://msdn2.microsoft.com/en-us/library/ms534302.aspx">MSDN Documentation</a>
* @see <a href="http://www.mozilla.org/docs/dom/domref/dom_el_ref20.html">Gecko DOM Reference</a>
* @see <a href="http://www.quirksmode.org/js/elementdimensions.html">Element Dimensions</a>
* @see <a href="http://www.w3.org/TR/REC-CSS2/box.html">Box Model</a>
* @see <a href="http://dump.testsuite.org/2006/dom/style/offset/spec">Reverse Engineering by Anne van Kesteren</a>
*/
public Object jsxGet_offsetParent() {
Object offsetParent = Context.getUndefinedValue();
DomNode currentElement = getDomNodeOrDie();
final HTMLElement htmlElement = (HTMLElement) currentElement.getScriptObject();
final ComputedCSSStyleDeclaration style = htmlElement.jsxGet_currentStyle();
final String position = style.getPositionWithInheritance();
final boolean ie = getBrowserVersion().isIE();
final boolean staticPos = "static".equals(position);
final boolean fixedPos = "fixed".equals(position);
final boolean useTables = ((ie && (staticPos || fixedPos)) || (!ie && staticPos));
while (currentElement != null) {
final DomNode parentNode = currentElement.getParentNode();
if (parentNode instanceof HtmlBody
|| (useTables && parentNode instanceof HtmlTableDataCell)
|| (useTables && parentNode instanceof HtmlTable)) {
offsetParent = parentNode.getScriptObject();
break;
}
if (parentNode != null && parentNode.getScriptObject() instanceof HTMLElement) {
final HTMLElement parentElement = (HTMLElement) parentNode.getScriptObject();
final ComputedCSSStyleDeclaration parentStyle = parentElement.jsxGet_currentStyle();
final String parentPosition = parentStyle.getPositionWithInheritance();
final boolean parentIsStatic = "static".equals(parentPosition);
final boolean parentIsFixed = "fixed".equals(parentPosition);
if ((ie && !parentIsStatic && !parentIsFixed) || (!ie && !parentIsStatic)) {
offsetParent = parentNode.getScriptObject();
break;
}
}
currentElement = currentElement.getParentNode();
}
return offsetParent;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "HTMLElement for " + getDomNodeOrNull();
}
/**
* Gets the scrollTop value for this element.
* @return the scrollTop value for this element
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534618.aspx">MSDN documentation</a>
*/
public int jsxGet_scrollTop() {
// It's easier to perform these checks and adjustments in the getter, rather than in the setter,
// because modifying the CSS style of the element is supposed to affect the attribute value.
if (scrollTop_ < 0) {
scrollTop_ = 0;
}
else if (scrollTop_ > 0) {
final String overflow = jsxGet_currentStyle().jsxGet_overflow();
if (!"scroll".equals(overflow) && !"auto".equals(overflow)) { // TODO: inherit, overflow-y
scrollTop_ = 0;
}
}
return scrollTop_;
}
/**
* Sets the scrollTop value for this element.
* @param scroll the scrollTop value for this element
*/
public void jsxSet_scrollTop(final int scroll) {
scrollTop_ = scroll;
}
/**
* Gets the scrollLeft value for this element.
* @return the scrollLeft value for this element
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534617.aspx">MSDN documentation</a>
*/
public int jsxGet_scrollLeft() {
// It's easier to perform these checks and adjustments in the getter, rather than in the setter,
// because modifying the CSS style of the element is supposed to affect the attribute value.
if (scrollLeft_ < 0) {
scrollLeft_ = 0;
}
else if (scrollLeft_ > 0) {
final String overflow = jsxGet_currentStyle().jsxGet_overflow();
if (!"scroll".equals(overflow) && !"auto".equals(overflow)) { // TODO: inherit, overflow-x
scrollLeft_ = 0;
}
}
return scrollLeft_;
}
/**
* Sets the scrollLeft value for this element.
* @param scroll the scrollLeft value for this element
*/
public void jsxSet_scrollLeft(final int scroll) {
scrollLeft_ = scroll;
}
/**
* Gets the scrollHeight for this element.
* @return a dummy value of 10
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534615.aspx">MSDN documentation</a>
*/
public int jsxGet_scrollHeight() {
return 10;
}
/**
* Gets the scrollWidth for this element.
* @return a dummy value of 10
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534619.aspx">MSDN documentation</a>
*/
public int jsxGet_scrollWidth() {
return 10;
}
/**
* Gets the namespace defined for the element.
* @return the namespace defined for the element
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534388.aspx">MSDN documentation</a>
*/
public String jsxGet_scopeName() {
final String prefix = getDomNodeOrDie().getPrefix();
return prefix != null ? prefix : "HTML";
}
/**
* Gets the Uniform Resource Name (URN) specified in the namespace declaration.
* @return the Uniform Resource Name (URN) specified in the namespace declaration
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534658.aspx">MSDN documentation</a>
*/
public String jsxGet_tagUrn() {
final String urn = getDomNodeOrDie().getNamespaceURI();
return urn != null ? urn : "";
}
/**
* Sets the Uniform Resource Name (URN) specified in the namespace declaration.
* @param tagUrn the Uniform Resource Name (URN) specified in the namespace declaration
* @see <a href="http://msdn.microsoft.com/en-us/library/ms534658.aspx">MSDN documentation</a>
*/
public void jsxSet_tagUrn(final String tagUrn) {
throw Context.reportRuntimeError("Error trying to set tagUrn to '" + tagUrn + "'.");
}
/**
* Gets the JavaScript property "parentElement".
* @return the parent element
* @see #jsxGet_parentNode()
*/
public HTMLElement jsxGet_parentElement() {
final Node parent = jsxGet_parentNode();
if (!(parent instanceof HTMLElement)) {
return null;
}
return (HTMLElement) parent;
}
/**
* Gets the first ancestor instance of {@link HTMLElement}. It is mostly identical
* to {@link #jsxGet_parentNode()} except that it skips XML nodes.
* @return the parent HTML element
* @see #jsxGet_parentNode()
*/
public HTMLElement getParentHTMLElement() {
Node parent = jsxGet_parentNode();
while (parent != null && !(parent instanceof HTMLElement)) {
parent = parent.jsxGet_parentNode();
}
return (HTMLElement) parent;
}
/**
* Implement the scrollIntoView() JavaScript function but don't actually do
* anything. The requirement
* is just to prevent scripts that call that method from failing
*/
public void jsxFunction_scrollIntoView() { }
/**
* Retrieves an object that specifies the bounds of a collection of TextRectangle objects.
* @see <a href="http://msdn.microsoft.com/en-us/library/ms536433.aspx">MSDN doc</a>
* @return an object that specifies the bounds of a collection of TextRectangle objects
*/
public TextRectangle jsxFunction_getBoundingClientRect() {
final TextRectangle textRectangle = new TextRectangle(0, getPosX() + jsxGet_clientLeft(), 0,
getPosY() + jsxGet_clientTop());
textRectangle.setParentScope(getWindow());
textRectangle.setPrototype(getPrototype(textRectangle.getClass()));
return textRectangle;
}
/**
* Retrieves a collection of rectangles that describes the layout of the contents of an object
* or range within the client. Each rectangle describes a single line.
* @return a collection of rectangles that describes the layout of the contents
*/
public Object jsxFunction_getClientRects() {
return new NativeArray(0);
}
/**
* Sets an expression for the specified HTMLElement.
*
* @param propertyName Specifies the name of the property to which expression is added
* @param expression specifies any valid script statement without quotations or semicolons
* This string can include references to other properties on the current page.
* Array references are not allowed on object properties included in this script.
* @param language specified the language used
*/
public void jsxFunction_setExpression(final String propertyName, final String expression, final String language) {
// Empty.
}
/**
* Removes the expression from the specified property.
*
* @param propertyName Specifies the name of the property from which to remove an expression
* @return true if the expression was successfully removed
*/
public boolean jsxFunction_removeExpression(final String propertyName) {
return true;
}
/**
* Retrieves an auto-generated, unique identifier for the object.
* <b>Note</b> The unique ID generated is not guaranteed to be the same every time the page is loaded.
* @return an auto-generated, unique identifier for the object
*/
public String jsxGet_uniqueID() {
if (uniqueID_ == null) {
uniqueID_ = "ms__id" + UniqueID_Counter_++;
}
return uniqueID_;
}
/**
* Dispatches an event into the event system (standards-conformant browsers only). See
* <a href="http://developer.mozilla.org/en/docs/DOM:element.dispatchEvent">the Gecko
* DOM reference</a> for more information.
*
* @param event the event to be dispatched
* @return <tt>false</tt> if at least one of the event handlers which handled the event
* called <tt>preventDefault</tt>; <tt>true</tt> otherwise
*/
public boolean jsxFunction_dispatchEvent(final Event event) {
event.setTarget(this);
final HtmlElement element = getDomNodeOrDie();
ScriptResult result = null;
if (event.jsxGet_type().equals(MouseEvent.TYPE_CLICK)) {
try {
element.click(event);
}
catch (final IOException e) {
throw Context.reportRuntimeError("Error calling click(): " + e.getMessage());
}
}
else {
result = fireEvent(event);
}
return !event.isAborted(result);
}
/**
* {@inheritDoc}
*/
@Override
public final HtmlElement getDomNodeOrDie() {
return (HtmlElement) super.getDomNodeOrDie();
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public HtmlElement getDomNodeOrNull() {
return (HtmlElement) super.getDomNodeOrNull();
}
/**
* Remove focus from this element.
*/
public void jsxFunction_blur() {
getDomNodeOrDie().blur();
}
/**
* Creates a new TextRange object for this element.
* @return a new TextRange object for this element
*/
public Object jsxFunction_createTextRange() {
final TextRange range = new TextRange(this);
range.setParentScope(getParentScope());
range.setPrototype(getPrototype(range.getClass()));
return range;
}
/**
* Checks whether the given element is contained within this object.
* @param element element object that specifies the element to check
* @return true if the element is contained within this object
*/
public boolean jsxFunction_contains(final HTMLElement element) {
for (HTMLElement parent = element; parent != null; parent = parent.jsxGet_parentElement()) {
if (this == parent) {
return true;
}
}
return false;
}
/**
* Sets the focus to this element.
*/
public void jsxFunction_focus() {
getDomNodeOrDie().focus();
}
/**
* Sets the object as active without setting focus to the object.
* @see <a href="http://msdn.microsoft.com/en-us/library/ms536738.aspx">MSDN documentation</a>
*/
public void jsxFunction_setActive() {
final Window window = getWindow();
final HTMLDocument document = window.jsxGet_document();
document.setActiveElement(this);
if (window.getWebWindow() == window.getWebWindow().getWebClient().getCurrentWindow()) {
final HtmlElement element = getDomNodeOrDie();
((HtmlPage) element.getPage()).setFocusedElement(element);
}
}
/**
* {@inheritDoc}
*/
@Override
public String jsxGet_nodeName() {
final DomNode domNode = getDomNodeOrDie();
String nodeName = domNode.getNodeName();
if (domNode.getPage() instanceof HtmlPage) {
nodeName = nodeName.toUpperCase();
}
return nodeName;
}
/**
* {@inheritDoc}
*/
@Override
public String jsxGet_prefix() {
if (getBrowserVersion().isIE()) {
return "";
}
return null;
}
/**
* Gets the filters.
* @return the filters
* @see <a href="http://msdn.microsoft.com/en-us/library/ms537452.aspx">MSDN doc</a>
*/
public Object jsxGet_filters() {
return this; // return anything, what matters is that it is not null
}
/**
* Click this element. This simulates the action of the user clicking with the mouse.
* @throws IOException if this click triggers a page load that encounters problems
*/
public void jsxFunction_click() throws IOException {
getDomNodeOrDie().click();
}
/**
* Returns the "spellcheck" property.
* @return the "spellcheck" property
*/
public boolean jsxGet_spellcheck() {
return Context.toBoolean(getDomNodeOrDie().getAttribute("spellcheck"));
}
/**
* Sets the "spellcheck" property.
* @param spellcheck the "spellcheck" property
*/
public void jsxSet_spellcheck(final boolean spellcheck) {
getDomNodeOrDie().setAttribute("spellcheck", Boolean.toString(spellcheck));
}
/**
* Returns the "lang" property.
* @return the "lang" property
*/
public String jsxGet_lang() {
return getDomNodeOrDie().getAttribute("lang");
}
/**
* Sets the "lang" property.
* @param lang the "lang" property
*/
public void jsxSet_lang(final String lang) {
getDomNodeOrDie().setAttribute("lang", lang);
}
/**
* Returns the "language" property.
* @return the "language" property
*/
public String jsxGet_language() {
return getDomNodeOrDie().getAttribute("language");
}
/**
* Sets the "language" property.
* @param language the "language" property
*/
public void jsxSet_language(final String language) {
getDomNodeOrDie().setAttribute("language", language);
}
/**
* Returns the "dir" property.
* @return the "dir" property
*/
public String jsxGet_dir() {
return getDomNodeOrDie().getAttribute("dir");
}
/**
* Sets the "dir" property.
* @param dir the "dir" property
*/
public void jsxSet_dir(final String dir) {
getDomNodeOrDie().setAttribute("dir", dir);
}
/**
* Returns the value of the tabIndex attribute.
* @return the value of the tabIndex attribute
*/
public int jsxGet_tabIndex() {
return (int) Context.toNumber(getDomNodeOrDie().getAttribute("tabindex"));
}
/**
* Sets the "tabIndex" property.
* @param tabIndex the "tabIndex" property
*/
public void jsxSet_tabIndex(final int tabIndex) {
getDomNodeOrDie().setAttribute("tabindex", Integer.toString(tabIndex));
}
/**
* Simulates a click on a scrollbar component (IE only).
* @param scrollAction the type of scroll action to simulate
*/
public void jsxFunction_doScroll(final String scrollAction) {
if (((HtmlPage) getDomNodeOrDie().getPage()).isBeingParsed()) {
throw Context.reportRuntimeError("The data necessary to complete this operation is not yet available.");
}
// Ignore because we aren't displaying anything!
}
/**
* Returns the "accessKey" property.
* @return the "accessKey" property
*/
public String jsxGet_accessKey() {
return getDomNodeOrDie().getAttribute("accesskey");
}
/**
* Sets the "accessKey" property.
* @param accessKey the "accessKey" property
*/
public void jsxSet_accessKey(final String accessKey) {
getDomNodeOrDie().setAttribute("accesskey", accessKey);
}
/**
* Returns the value of the specified attribute (width or height).
* @return the value of the specified attribute (width or height)
* @param attributeName the name of the attribute to return (<tt>"width"</tt> or <tt>"height"</tt>)
* @param returnNegativeValues if <tt>true</tt>, negative values are returned;
* if <tt>false</tt>, this method returns an empty string in lieu of negative values;
* if <tt>null</tt>, this method returns <tt>0</tt> in lieu of negative values
*/
protected String getWidthOrHeight(final String attributeName, final Boolean returnNegativeValues) {
String s = getDomNodeOrDie().getAttribute(attributeName);
if (!s.matches("\\d+%")) {
try {
final Float f = Float.parseFloat(s);
final Integer i = f.intValue();
if (i < 0) {
if (returnNegativeValues == null) {
s = "0";
}
else if (!returnNegativeValues) {
s = "";
}
else {
s = String.valueOf(i);
}
}
else {
s = String.valueOf(i);
}
}
catch (final NumberFormatException e) {
if (getBrowserVersion().isIE()) {
s = "";
}
}
}
return s;
}
/**
* Sets the value of the specified attribute (width or height).
* @param attributeName the name of the attribute to set (<tt>"width"</tt> or <tt>"height"</tt>)
* @param value the value of the specified attribute (width or height)
* @param allowNegativeValues if <tt>true</tt>, negative values will be stored;
* if <tt>false</tt>, negative values cause an exception to be thrown;
* if <tt>null</tt>, negative values set the value to <tt>0</tt>
*/
protected void setWidthOrHeight(final String attributeName, String value, final Boolean allowNegativeValues) {
if (value.endsWith("px")) {
value = value.substring(0, value.length() - 2);
}
if (getBrowserVersion().isIE()) {
boolean error = false;
if (!value.matches("\\d+%")) {
try {
final Float f = Float.parseFloat(value);
final Integer i = f.intValue();
if (i < 0) {
if (allowNegativeValues == null) {
value = "0";
}
else if (!allowNegativeValues) {
error = true;
}
}
}
catch (final NumberFormatException e) {
error = true;
}
}
if (error) {
final Exception e = new Exception("Cannot set the width property to invalid value: " + value);
Context.throwAsScriptRuntimeEx(e);
}
}
getDomNodeOrDie().setAttribute(attributeName, value);
}
/**
* Sets the specified color attribute to the specified value.
* @param name the color attribute's name
* @param value the color attribute's value
*/
protected void setColorAttribute(final String name, final String value) {
final String s;
if (isHexadecimalColor(value)) {
s = value;
}
else {
s = "#000000";
}
this.getDomNodeOrDie().setAttribute(name, s);
}
/**
* Returns <tt>true</tt> if the specified string is an RGB color in hexadecimal notation.
* @param s the string to check
* @return <tt>true</tt> if the specified string is an RGB color in hexadecimal notation
*/
private static boolean isHexadecimalColor(final String s) {
return s.toLowerCase().matches("#([0-9a-f]{6})");
}
/**
* Returns the value of the "align" property.
* @param returnInvalidValues if <tt>true</tt>, this method will return any value, including technically
* invalid values; if <tt>false</tt>, this method will return an empty string instead of invalid values
* @return the value of the "align" property
*/
protected String getAlign(final boolean returnInvalidValues) {
final String align = getDomNodeOrDie().getAttribute("align");
if (align.equals("center")
|| align.equals("justify")
|| align.equals("left")
|| align.equals("right")
|| returnInvalidValues) {
return align;
}
return "";
}
/**
* Sets the value of the "align" property.
* @param align the value of the "align" property
* @param ignoreIfNoError if <tt>true</tt>, the invocation will be a no-op if it does not trigger an error
* (i.e., it will not actually set the align attribute)
*/
protected void setAlign(String align, final boolean ignoreIfNoError) {
align = align.toLowerCase();
final boolean ff = getBrowserVersion().isFirefox();
if (ff || align.equals("center") || align.equals("justify") || align.equals("left") || align.equals("right")) {
if (!ignoreIfNoError) {
getDomNodeOrDie().setAttribute("align", align);
}
}
else {
throw Context.reportRuntimeError("Cannot set the align property to invalid value: " + align);
}
}
/**
* Returns the value of the "vAlign" property.
* @param valid the valid values; if <tt>null</tt>, any value is valid
* @param defaultValue the default value to use, if necessary
* @return the value of the "vAlign" property
*/
protected String getVAlign(final String[] valid, final String defaultValue) {
final String valign = getDomNodeOrDie().getAttribute("valign");
if (valid == null || ArrayUtils.contains(valid, valign)) {
return valign;
}
return defaultValue;
}
/**
* Sets the value of the "vAlign" property.
* @param vAlign the value of the "vAlign" property
* @param valid the valid values; if <tt>null</tt>, any value is valid
*/
protected void setVAlign(final Object vAlign, final String[] valid) {
final String s = Context.toString(vAlign).toLowerCase();
if (valid == null || ArrayUtils.contains(valid, s)) {
getDomNodeOrDie().setAttribute("valign", s);
}
else {
throw Context.reportRuntimeError("Cannot set the vAlign property to invalid value: " + vAlign);
}
}
/**
* Returns the value of the "ch" property.
* @return the value of the "ch" property
*/
protected String getCh() {
final boolean ie = getBrowserVersion().isIE();
if (ie) {
return ch_;
}
String ch = getDomNodeOrDie().getAttribute("char");
if (ch == DomElement.ATTRIBUTE_NOT_DEFINED) {
ch = ".";
}
return ch;
}
/**
* Sets the value of the "ch" property.
* @param ch the value of the "ch" property
*/
protected void setCh(final String ch) {
final boolean ie = getBrowserVersion().isIE();
if (ie) {
ch_ = ch;
}
else {
getDomNodeOrDie().setAttribute("char", ch);
}
}
/**
* Returns the value of the "chOff" property.
* @return the value of the "chOff" property
*/
protected String getChOff() {
final boolean ie = getBrowserVersion().isIE();
if (ie) {
return chOff_;
}
return getDomNodeOrDie().getAttribute("charOff");
}
/**
* Sets the value of the "chOff" property.
* @param chOff the value of the "chOff" property
*/
protected void setChOff(String chOff) {
final boolean ie = getBrowserVersion().isIE();
if (ie) {
chOff_ = chOff;
}
else {
try {
Float f = new Float(chOff);
if (f < 0) {
f = 0f;
}
chOff = String.valueOf(f.intValue());
}
catch (final NumberFormatException e) {
// Ignore.
}
getDomNodeOrDie().setAttribute("charOff", chOff);
}
}
}