/* * Copyright (c) 2002-2008 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; 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.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.LogFactory; import org.mozilla.javascript.BaseFunction; import org.mozilla.javascript.Context; import org.mozilla.javascript.ContextAction; import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.Function; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.Scriptable; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import com.gargoylesoftware.htmlunit.BrowserVersion; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.WebRequestSettings; import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.html.ClickableElement; 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.DomNode; import com.gargoylesoftware.htmlunit.html.DomText; import com.gargoylesoftware.htmlunit.html.HTMLParser; 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; /** * The JavaScript object "HTMLElement" which is the base class for all HTML * objects. This will typically wrap an instance of {@link HtmlElement}. * * @version $Revision$ * @author Mike Bowler * @author David K. Taylor * @author Barnaby Court * @author Christian Sell * @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 int BEHAVIOR_ID_UNKNOWN = -1; static final int BEHAVIOR_ID_CLIENT_CAPS = 0; static final int BEHAVIOR_ID_HOMEPAGE = 1; 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 behaviors_ = new HashSet(); 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_; /** * The tag names of the objects for which outerHTML is readonly */ private static final List OUTER_HTML_READONLY = Arrays.asList(new String[] { "caption", "col", "colgroup", "frameset", "html", "tbody", "td", "tfoot", "th", "thead", "tr"}); private CSSStyleDeclaration style_; /** * Creates an instance. */ public HTMLElement() { // Empty. } /** * 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 */ protected 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.getAttributesCollection()) { 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 = (HtmlElement) 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 getHtmlElementOrDie().getId(); } /** * Sets the identifier this element. * @param newId the new identifier of this element */ public void jsxSet_id(final String newId) { getHtmlElementOrDie().setId(newId); } /** * Returns the element title. * @return the ID of this element */ public String jsxGet_title() { return getHtmlElementOrDie().getAttributeValue("title"); } /** * Sets the title of this element. * @param newTitle the new identifier of this element */ public void jsxSet_title(final String newTitle) { getHtmlElementOrDie().setAttributeValue("title", newTitle); } /** * Returns true if this element is disabled. * @return true if this element is disabled */ public boolean jsxGet_disabled() { return getHtmlElementOrDie().isAttributeDefined("disabled"); } /** * 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 = getHtmlElementOrDie(); if (disabled) { element.setAttributeValue("disabled", "disabled"); } else { element.removeAttribute("disabled"); } } /** * Returns the tag name of this element. * @return the tag name in uppercase */ public String jsxGet_tagName() { String tagName = getHtmlElementOrDie().getTagName(); if (jsxGet_namespaceURI() == null) { tagName = tagName.toUpperCase(); } return tagName; } /** * Returns The URI that identifies an XML namespace. * @return the URI that identifies an XML namespace */ public String jsxGet_namespaceURI() { return getHtmlElementOrDie().getNamespaceURI(); } /** * Returns The local name (without prefix). * @return the local name (without prefix) */ public String jsxGet_localName() { String localName = getHtmlElementOrDie().getLocalName(); if (jsxGet_namespaceURI() == null) { localName = localName.toUpperCase(); } return localName; } /** * Returns The Namespace prefix. * @return the Namespace prefix */ public String jsxGet_prefix() { return getHtmlElementOrDie().getPrefix(); } /** * Looks at attributes with the given name. * {@inheritDoc} */ public Object getWithFallback(final String name) { final HtmlElement htmlElement = getHtmlElementOrNull(); if (htmlElement != null && isAttributeName(name)) { final String value = htmlElement.getAttributeValue(name); if (HtmlElement.ATTRIBUTE_NOT_DEFINED != value) { getLog().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 false 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); } /** * Returns a collection of the attributes of this element. * @return a collection of the attributes of this element * @see Gecko DOM Reference */ public NamedNodeMap jsxGet_attributes() { return new NamedNodeMap(getHtmlElementOrDie()); } /** * Gets the specified attribute. * @param attributeName attribute name * @return the value of the specified attribute, null if the attribute is not defined */ public String jsxFunction_getAttribute(final String attributeName) { final String value = getHtmlElementOrDie().getAttributeValue(attributeName); if (value == HtmlElement.ATTRIBUTE_NOT_DEFINED) { return null; } return value; } /** * 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, null if the attribute is not defined */ public String jsxFunction_getAttributeNS(final String namespaceURI, final String localName) { return getHtmlElementOrDie().getAttributeNS(namespaceURI, localName); } /** * Test for attribute. * See also * the DOM reference * * @param name Name of the attribute to test * @return true if the node has this attribute */ public boolean jsxFunction_hasAttribute(final String name) { return getHtmlElementOrDie().getAttribute(name) != HtmlElement.ATTRIBUTE_NOT_DEFINED; } /** * Test for attribute. * See also * the DOM reference * * @param namespaceURI the namespace URI * @param localName the local name of the attribute to look for * @return true if the node has this attribute */ public boolean jsxFunction_hasAttributeNS(final String namespaceURI, final String localName) { return getHtmlElementOrDie().getAttributeNS(namespaceURI, localName) != HtmlElement.ATTRIBUTE_NOT_DEFINED; } /** * Sets an attribute. * See also * the DOM reference * * @param name Name of the attribute to set * @param value Value to set the attribute to */ public void jsxFunction_setAttribute(final String name, final String value) { getHtmlElementOrDie().setAttributeValue(name, value); //FF: call corresponding event handler jsxSet_onxxx if found if (getBrowserVersion().isNetscape()) { 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 local 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) { getHtmlElementOrDie().setAttributeValue(namespaceURI, qualifiedName, value); } /** * Remove an attribute. * * @param name Name of the attribute to remove */ public void jsxFunction_removeAttribute(final String name) { getHtmlElementOrDie().removeAttribute(name); } /** * 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 */ public Object jsxFunction_getAttributeNode(final String attributeName) { final Attr att = new Attr(); att.setPrototype(getPrototype(Attr.class)); att.setParentScope(getWindow()); att.init(attributeName, getHtmlElementOrDie()); return att; } /** * 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); replacedAtt.detachFromParent(); getHtmlElementOrDie().setAttributeValue(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 */ public Object jsxFunction_getElementsByTagName(final String tagName) { final DomNode node = getDomNodeOrDie(); final HTMLCollection collection = new HTMLCollection(this); final String xpath; if ("*".equals(tagName)) { xpath = ".//*"; } else { xpath = ".//node()[name() = '" + tagName.toLowerCase() + "']"; } collection.init(node, xpath); return collection; } /** * Returns the class defined for this element. * @return the class name */ public Object jsxGet_className() { return getHtmlElementOrDie().getAttributeValue("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); } /** * Sets the class attribute for this element. * @param className - the new class name */ public void jsxSet_className(final String className) { getHtmlElementOrDie().setAttributeValue("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 textContent attribute. * @return the contents of this node as text */ public String jsxGet_textContent() { return jsxGet_innerText(); } /** * Gets the outerHTML of the node. * @see * MSDN documentation * @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(""); } 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.getAttributesCollection()) { final String name = attr.getName(); final String value = attr.getValue().replaceAll("\"", """); final boolean quote = !ie || com.gargoylesoftware.htmlunit.util.StringUtils.containsWhitespace(value); 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(""); } 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(); domNode.removeAllChildren(); final BrowserVersion browserVersion = getBrowserVersion(); // null && IE -> add child // null && non-IE -> Don't add // '' -> Don't add if ((value == null && browserVersion.isIE()) || (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 && getWindow().getWebWindow().getWebClient().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 */ public void jsxSet_innerText(final String value) { final DomNode domNode = getDomNodeOrDie(); domNode.removeAllChildren(); final DomNode node = new DomText(getDomNodeOrDie().getPage(), value); domNode.appendChild(node); //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 */ public void jsxSet_textContent(final String value) { jsxSet_innerText(value); } /** * Replace all children elements of this element with the supplied value. * Sets the outerHTML of the node. * @see * MSDN documentation * @param value - the new value for replacing this node */ 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()); } parseHtmlSnippet(domNode, 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 true, append the parsed content as a child of the specified target; * if false, append the parsed content as the previous sibling of the specified target * @param source the HTML code extract to parse */ static void parseHtmlSnippet(final DomNode target, final boolean append, final String source) { final HtmlPage page = (HtmlPage) target.getPage(); final DomNode proxyNode = new HtmlDivision(null, HtmlDivision.TAG_NAME, page, null) { private static final long serialVersionUID = 2108037256628269797L; @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; } }; try { HTMLParser.parseFragment(proxyNode, 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()); } } /** * 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.getAttributesCollection()) { 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 * MSDN documentation * @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 parseHtmlSnippet(node, append, text); } /** * Inserts the given element into the element at the location. * @see * MSDN documentation * @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: *
    *
  • #default#clientCaps
  • *
  • #default#homePage
  • *
  • #default#download
  • *
* @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 { getLog().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: getLog().warn("Unexpected behavior id: " + id + ". Ignoring."); } } //----------------------- START #default#clientCaps BEHAVIOR ----------------------- /** * Returns the screen's available height. Part of the #default#clientCaps * 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 #default#clientCaps * 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 #default#clientCaps * 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 #default#clientCaps * 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 #default#clientCaps * default IE behavior implementation. * @return the connection type being used * Current implementation always return "modem" */ public String getConnectionType() { return "modem"; } /** * Returns true if cookies are enabled. Part of the #default#clientCaps * 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 #default#clientCaps * 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 #default#clientCaps * default IE behavior implementation. * @return the screen's height */ public int getHeight() { return getWindow().jsxGet_screen().jsxGet_height(); } /** * Returns true if Java is enabled. Part of the #default#clientCaps * 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 #default#clientCaps * default IE behavior implementation. * @return the platform used */ public String getPlatform() { return getWindow().jsxGet_navigator().jsxGet_platform(); } /** * Returns the system language. Part of the #default#clientCaps * default IE behavior implementation. * @return the system language */ public String getSystemLanguage() { return getWindow().jsxGet_navigator().jsxGet_systemLanguage(); } /** * Returns the user language. Part of the #default#clientCaps * 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 #default#clientCaps * 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 #default#clientCaps 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) { getLog().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 #default#clientCaps * default IE behavior implementation. */ public void clearComponentRequest() { getLog().debug("Call to clearComponentRequest() ignored."); } /** * Compares the two specified version numbers. Part of the #default#clientCaps * 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 true if the components are downloaded successfully * Current implementation always return false */ 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 true 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 true 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 * MSDN documentation * @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().getUrl().getHost().equals(url.getHost())) { throw Context.reportRuntimeError("Not authorized url: " + url); } final Thread t = new DownloadBehaviorDownloader(url, callback); getLog().debug("Starting download thread for " + url); t.start(); } /** * 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 Stefan Anzinger */ private final class DownloadBehaviorDownloader extends Thread { private final URL url_; private final Function callback_; /** * @param url the URL to download * @param callback the function to callback */ private DownloadBehaviorDownloader(final URL url, final Function callback) { super("Downloader for behavior #default#download '" + url + "'"); url_ = url; callback_ = callback; } /** * Performs the download and calls the callback method. */ @Override public void run() { final WebClient wc = getWindow().getWebWindow().getWebClient(); final Scriptable scope = callback_.getParentScope(); final WebRequestSettings settings = new WebRequestSettings(url_); try { final WebResponse webResponse = wc.loadWebResponse(settings); final String content = webResponse.getContentAsString(); getLog().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; } }; ContextFactory.getGlobal().call(action); } catch (final IOException e) { getLog().error("Behavior #default#download: Cannot download " + url_, e); } } } //----------------------- END #default#download BEHAVIOR ----------------------- //----------------------- START #default#homePage BEHAVIOR ----------------------- /** * Returns true 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 #default#homePage default IE behavior * implementation. * @param url the URL to check * @return true 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().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 #default#homePage * 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 * #default#homePage 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 * MSDN documentation * @return the child at the given position */ public Object jsxGet_children() { final HTMLCollection children = new HTMLCollection(this); children.init(getDomNodeOrDie(), "./*"); return children; } /** * Returns this element's offsetHeight, 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 offsetHeight * @see MSDN Documentation * @see Element Dimensions */ public int jsxGet_offsetHeight() { final MouseEvent event = MouseEvent.getCurrentMouseEvent(); if (isAncestorOfEventTarget(event)) { // compute appropriate offsetHeight to make as if mouse event produced within this element return event.jsxGet_clientY() - getPosY() + 50; } return jsxGet_currentStyle().getCalculatedHeight(true, true); } /** * Returns this element's offsetWidth, 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 offsetWidth * @see MSDN Documentation * @see Element Dimensions */ public int jsxGet_offsetWidth() { final MouseEvent event = MouseEvent.getCurrentMouseEvent(); if (isAncestorOfEventTarget(event)) { // compute appropriate offsetwidth to make as if mouse event produced within this element return event.jsxGet_clientX() - getPosX() + 50; } return jsxGet_currentStyle().getCalculatedWidth(true, true); } /** * Returns true 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 true if this element's node is an ancestor of the specified event's target node */ private boolean isAncestorOfEventTarget(final MouseEvent event) { if (event == null) { return false; } final HTMLElement target = (HTMLElement) event.jsxGet_target(); return getHtmlElementOrDie().isAncestorOf(target.getHtmlElementOrDie()); } /** * Returns this element's X position. * @return this element's X position */ int getPosX() { int cumulativeOffset = 0; HTMLElement element = this; while (element != null) { cumulativeOffset += element.jsxGet_offsetLeft(); element = element.jsxGet_offsetParent(); } return cumulativeOffset; } /** * Returns this element's Y position. * @return this element's Y position */ int getPosY() { int cumulativeOffset = 0; HTMLElement element = this; while (element != null) { cumulativeOffset += element.jsxGet_offsetTop(); element = element.jsxGet_offsetParent(); } return cumulativeOffset; } /** * Returns this element's offsetLeft, which is the calculated left position of this * element relative to the offsetParent. * * @return this element's offsetLeft * @see MSDN Documentation * @see Element Dimensions * @see Reverse Engineering by Anne van Kesteren */ public int jsxGet_offsetLeft() { if (this instanceof HTMLBodyElement) { return 0; } int left = 0; final HTMLElement offsetParent = jsxGet_offsetParent(); // 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().jsxGet_position(); 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(); } // Add the offset for the final ancestor node (the offset parent). if (node != null && node.getScriptObject() instanceof HTMLElement) { left += offsetParent.jsxGet_currentStyle().getLeft(true, false, true); } return left; } /** * Returns this element's offsetTop, which is the calculated top position of this * element relative to the offsetParent. * * @return this element's offsetTop * @see MSDN Documentation * @see Element Dimensions * @see Reverse Engineering by Anne van Kesteren */ public int jsxGet_offsetTop() { if (this instanceof HTMLBodyElement) { return 0; } int top = 0; final HTMLElement offsetParent = jsxGet_offsetParent(); // 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().jsxGet_position(); 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(); } // Add the offset for the final ancestor node (the offset parent). if (node != null && node.getScriptObject() instanceof HTMLElement) { top += offsetParent.jsxGet_currentStyle().getTop(false, false, true); } return top; } /** * Returns this element's offsetParent. The offsetLeft and * offsetTop attributes are relative to the offsetParent. * * @return this element's offsetParent * @see MSDN Documentation * @see Gecko DOM Reference * @see Element Dimensions * @see Box Model * @see Reverse Engineering by Anne van Kesteren */ public HTMLElement jsxGet_offsetParent() { HTMLElement offsetParent = null; DomNode currentElement = getHtmlElementOrDie(); final HTMLElement htmlElement = (HTMLElement) currentElement.getScriptObject(); final ComputedCSSStyleDeclaration style = htmlElement.jsxGet_currentStyle(); final String position = style.jsxGet_position(); 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 = (HTMLElement) 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.jsxGet_position(); final boolean parentIsStatic = "static".equals(parentPosition); final boolean parentIsFixed = "fixed".equals(parentPosition); if ((ie && !parentIsStatic && !parentIsFixed) || (!ie && !parentIsStatic)) { offsetParent = (HTMLElement) parentNode.getScriptObject(); break; } } currentElement = currentElement.getParentNode(); } return offsetParent; } /** * {@inheritDoc} */ @Override public String toString() { return "HTMLElement for " + getHtmlElementOrNull(); } /** * Gets the scrollTop for this element. * @return a dummy value (default is 0) * @see * MSDN documentation */ public int jsxGet_scrollTop() { return scrollTop_; } /** * Sets the scrollTop for this element. * @param scroll the new value */ public void jsxSet_scrollTop(final int scroll) { scrollTop_ = scroll; } /** * Gets the scrollLeft for this element. * @return a dummy value (default is 0) * @see * MSDN documentation */ public int jsxGet_scrollLeft() { return scrollLeft_; } /** * Sets the scrollLeft for this element. * @param scroll the new value */ public void jsxSet_scrollLeft(final int scroll) { scrollLeft_ = scroll; } /** * Gets the scrollHeight for this element. * @return a dummy value of 10 * @see * MSDN documentation */ public int jsxGet_scrollHeight() { return 10; } /** * Gets the scrollWidth for this element. * @return a dummy value of 10 * @see * MSDN documentation */ public int jsxGet_scrollWidth() { return 10; } /** * Gets the JavaScript property "parentElement". *

It is identical to {@link #jsxGet_parentNode()} * with the exception of HTML, which has a null parent element. * @return the parent element * @see #jsxGet_parentNode() */ public Object jsxGet_parentElement() { if ("html".equalsIgnoreCase(getDomNodeOrDie().getNodeName())) { return null; } return jsxGet_parentNode(); } /** * 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. * @return an object that specifies the bounds of a collection of TextRectangle objects */ public TextRectangle jsxFunction_getBoundingClientRect() { final TextRectangle textRectangle = new TextRectangle(); 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. * Note 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 * the Gecko * DOM reference for more information. * * @param event the event to be dispatched * @return false if at least one of the event handlers which handled the event * called preventDefault; true otherwise */ public boolean jsxFunction_dispatchEvent(final Event event) { event.setTarget(this); final HtmlElement element = getHtmlElementOrDie(); if (event instanceof MouseEvent && element instanceof ClickableElement) { if (event.jsxGet_type().equals(MouseEvent.TYPE_CLICK)) { try { ((ClickableElement) element).click(event); } catch (final IOException e) { throw Context.reportRuntimeError("Error calling click(): " + e.getMessage()); } } else if (event.jsxGet_type().equals(MouseEvent.TYPE_DBL_CLICK)) { try { ((ClickableElement) element).dblClick(event.jsxGet_shiftKey(), event.jsxGet_ctrlKey(), event.jsxGet_altKey()); } catch (final IOException e) { throw Context.reportRuntimeError("Error calling dblClick(): " + e.getMessage()); } } else { fireEvent(event); } } else { fireEvent(event); } return !event.isPreventDefault(); } /** * Returns the HTML element that corresponds to this JavaScript object or throw an exception * if one cannot be found. * @return the HTML element * @exception IllegalStateException If the HTML element could not be found. */ public final HtmlElement getHtmlElementOrDie() throws IllegalStateException { return (HtmlElement) getDomNodeOrDie(); } /** * Returns the HTML element that corresponds to this JavaScript object * or null if an element hasn't been set. * @return the HTML element or null */ public final HtmlElement getHtmlElementOrNull() { return (HtmlElement) getDomNodeOrNull(); } /** * Remove focus from this element. */ public void jsxFunction_blur() { final HtmlElement element = (HtmlElement) getDomNodeOrDie(); element.blur(); } /** * Sets the focus to this element. */ public void jsxFunction_focus() { final HtmlElement element = (HtmlElement) getDomNodeOrDie(); element.focus(); } /** * Sets the object as active without setting focus to the object. * @see MSDN documentation */ 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 = (HtmlElement) getDomNodeOrDie(); ((HtmlPage) element.getPage()).setFocusedElement(element); } } }