/* * 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.util.ArrayList; import java.util.List; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import com.gargoylesoftware.htmlunit.ScriptResult; import com.gargoylesoftware.htmlunit.SgmlPage; import com.gargoylesoftware.htmlunit.html.DomDocumentFragment; import com.gargoylesoftware.htmlunit.html.DomNode; import com.gargoylesoftware.htmlunit.html.HtmlElement; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable; /** * The JavaScript object "Node" which is the base class for all DOM * objects. This will typically wrap an instance of {@link DomNode}. * * @version $Revision$ * @author Mike Bowler * @author David K. Taylor * @author Barnaby Court * @author Christian Sell * @author George Murnock * @author Chris Erskine * @author Bruce Faulkner * @author Ahmed Ashour */ public class Node extends SimpleScriptable { private HTMLCollection childNodes_; //has to be a member to have equality (==) working private static final long serialVersionUID = -5695262053081637445L; private EventListenersContainer eventListenersContainer_; /** @see org.w3c.dom.Node#ELEMENT_NODE */ public static final short ELEMENT_NODE = org.w3c.dom.Node.ELEMENT_NODE; /** @see org.w3c.dom.Node#ATTRIBUTE_NODE */ public static final short ATTRIBUTE_NODE = org.w3c.dom.Node.ATTRIBUTE_NODE; /** @see org.w3c.dom.Node#TEXT_NODE */ public static final short TEXT_NODE = org.w3c.dom.Node.TEXT_NODE; /** @see org.w3c.dom.Node#CDATA_SECTION_NODE */ public static final short CDATA_SECTION_NODE = org.w3c.dom.Node.CDATA_SECTION_NODE; /** @see org.w3c.dom.Node#ENTITY_REFERENCE_NODE */ public static final short ENTITY_REFERENCE_NODE = org.w3c.dom.Node.ENTITY_REFERENCE_NODE; /** @see org.w3c.dom.Node#ENTITY_NODE */ public static final short ENTITY_NODE = org.w3c.dom.Node.ENTITY_NODE; /** @see org.w3c.dom.Node#PROCESSING_INSTRUCTION_NODE */ public static final short PROCESSING_INSTRUCTION_NODE = org.w3c.dom.Node.PROCESSING_INSTRUCTION_NODE; /** @see org.w3c.dom.Node#COMMENT_NODE */ public static final short COMMENT_NODE = org.w3c.dom.Node.COMMENT_NODE; /** @see org.w3c.dom.Node#DOCUMENT_NODE */ public static final short DOCUMENT_NODE = org.w3c.dom.Node.DOCUMENT_NODE; /** @see org.w3c.dom.Node#DOCUMENT_TYPE_NODE */ public static final short DOCUMENT_TYPE_NODE = org.w3c.dom.Node.DOCUMENT_TYPE_NODE; /** @see org.w3c.dom.Node#DOCUMENT_FRAGMENT_NODE */ public static final short DOCUMENT_FRAGMENT_NODE = org.w3c.dom.Node.DOCUMENT_FRAGMENT_NODE; /** @see org.w3c.dom.Node#NOTATION_NODE */ public static final short NOTATION_NODE = org.w3c.dom.Node.NOTATION_NODE; /** * Creates an instance. */ public Node() { // Empty. } /** * Gets the JavaScript property "nodeType" for the current node. * @return the node type */ public short jsxGet_nodeType() { return getDomNodeOrDie().getNodeType(); } /** * Gets the JavaScript property "nodeName" for the current node. * @return the node name */ public String jsxGet_nodeName() { final DomNode domNode = getDomNodeOrDie(); String nodeName = domNode.getNodeName(); // If this is an HtmlElement then flip the result to uppercase. This should really be // changed in HtmlElement itself but that would break backwards compatibility fairly // significantly as that one is documented as always returning a lowercase value. // Update: not sure how accurate the previous comment is. It seems that uppercase only // applies within an Html document and not for HtmlElement within an xml document if (domNode instanceof HtmlElement && ((HtmlElement) domNode).getNamespaceURI() == null && (domNode.getOwnerDocument() instanceof HtmlPage)) { nodeName = nodeName.toUpperCase(); } return nodeName; } /** * Gets the JavaScript property "nodeValue" for the current node. * @return the node value */ public String jsxGet_nodeValue() { return getDomNodeOrDie().getNodeValue(); } /** * Sets the JavaScript property "nodeValue" for the current node. * @param newValue the new node value */ public void jsxSet_nodeValue(final String newValue) { getDomNodeOrDie().setNodeValue(newValue); } /** * Adds a DOM node to the node. * @param childObject the node to add to this node * @return the newly added child node */ public Object jsxFunction_appendChild(final Object childObject) { Object appendedChild = null; if (childObject instanceof Node) { // Get XML node for the DOM node passed in final DomNode childDomNode = ((Node) childObject).getDomNodeOrDie(); // Get the parent XML node that the child should be added to. final DomNode parentNode = getDomNodeOrDie(); // Append the child to the parent node parentNode.appendChild(childDomNode); appendedChild = childObject; //if the parentNode has null parentNode in IE, //create a DocumentFragment to be the parentNode's parentNode. if (!(parentNode instanceof SgmlPage) && !(this instanceof DocumentFragment) && parentNode.getParentNode() == null && getBrowserVersion().isIE()) { final DomDocumentFragment fragment = ((SgmlPage) parentNode.getPage()).createDomDocumentFragment(); fragment.appendChild(parentNode); } } return appendedChild; } /** * Clones this node. * @param deep if true, recursively clones all descendants * @return the newly cloned node */ public Object jsxFunction_cloneNode(final boolean deep) { final DomNode domNode = getDomNodeOrDie(); final DomNode clonedNode = domNode.cloneNode(deep); return getJavaScriptNode(clonedNode); } /** * Add a DOM node as a child to this node before the referenced * node. If the referenced node is null, append to the end. * @param newChildObject the node to add to this node * @param refChildObject the node before which to add the new child * @return the newly added child node */ public Object jsxFunction_insertBefore( final Object newChildObject, final Object refChildObject) { Object appendedChild = null; if (newChildObject instanceof Node) { final DomNode newChildNode = ((Node) newChildObject).getDomNodeOrDie(); if (newChildNode instanceof DomDocumentFragment) { final DomDocumentFragment fragment = (DomDocumentFragment) newChildNode; for (final DomNode child : fragment.getChildren()) { jsxFunction_insertBefore(child.getScriptObject(), refChildObject); } return newChildObject; } final DomNode refChildNode; // IE accepts non standard calls with only one arg if (Context.getUndefinedValue().equals(refChildObject)) { if (getBrowserVersion().isIE()) { refChildNode = null; } else { throw Context.reportRuntimeError("insertBefore: not enough arguments"); } } else if (refChildObject != null) { refChildNode = ((Node) refChildObject).getDomNodeOrDie(); } else { refChildNode = null; } // Append the child to the parent node if (refChildNode != null) { refChildNode.insertBefore(newChildNode); appendedChild = newChildObject; } else { getDomNodeOrDie().appendChild(newChildNode); } //if parentNode is null in IE, create a DocumentFragment to be the parentNode if (getDomNodeOrDie().getParentNode() == null && getWindow().getWebWindow().getWebClient().getBrowserVersion().isIE()) { final DomDocumentFragment fragment = ((HtmlPage) getDomNodeOrDie().getPage()).createDomDocumentFragment(); fragment.appendChild(getDomNodeOrDie()); } } return appendedChild; } /** * This method provides a way to determine whether two Node references returned by * the implementation reference the same object. * When two Node references are references to the same object, even if through a proxy, * the references may be used completely interchangeably, such that all attributes * have the same values and calling the same DOM method on either reference always has exactly the same effect. * * @param other the node to test against * * @return whether this node is the same node as the given one */ public boolean jsxFunction_isSameNode(final Object other) { return other == this; } /** * Removes a DOM node from this node. * @param childObject the node to remove from this node * @return the removed child node */ public Object jsxFunction_removeChild(final Object childObject) { Object removedChild = null; if (childObject instanceof Node) { // Get XML node for the DOM node passed in final DomNode childNode = ((Node) childObject).getDomNodeOrDie(); // Remove the child from the parent node childNode.remove(); removedChild = childObject; } return removedChild; } /** * Returns whether this node has any children. * @return boolean true if this node has any children, false otherwise */ public boolean jsxFunction_hasChildNodes() { return getDomNodeOrDie().getChildren().iterator().hasNext(); } /** * Returns the child nodes of the current element. * @return the child nodes of the current element */ public Object jsxGet_childNodes() { if (childNodes_ == null) { childNodes_ = new HTMLCollection(this); childNodes_.initFromChildren(getDomNodeOrDie()); } return childNodes_; } /** * Replaces a child DOM node with another DOM node. * @param newChildObject the node to add as a child of this node * @param oldChildObject the node to remove as a child of this node * @return the removed child node */ public Object jsxFunction_replaceChild(final Object newChildObject, final Object oldChildObject) { Object removedChild = null; if (newChildObject instanceof DocumentFragment) { final DocumentFragment fragment = (DocumentFragment) newChildObject; Node firstNode = null; final Node refChildObject = (Node) ((Node) oldChildObject).jsxGet_nextSibling(); for (final DomNode node : fragment.getDomNodeOrDie().getChildren()) { if (firstNode == null) { jsxFunction_replaceChild(node.getScriptObject(), oldChildObject); firstNode = (Node) node.getScriptObject(); } else { jsxFunction_insertBefore(node.getScriptObject(), refChildObject); } } if (firstNode == null) { jsxFunction_removeChild(oldChildObject); } removedChild = oldChildObject; } else if (newChildObject instanceof Node && oldChildObject instanceof Node) { // Get XML nodes for the DOM nodes passed in final DomNode newChildNode = ((Node) newChildObject).getDomNodeOrDie(); final DomNode oldChildNode; if (oldChildObject != null) { // Replace the old child with the new child. oldChildNode = ((Node) oldChildObject).getDomNodeOrDie(); oldChildNode.replace(newChildNode); removedChild = oldChildObject; } } return removedChild; } /** * Gets the JavaScript property "parentNode" for the node that * contains the current node. * @return the parent node */ public Object jsxGet_parentNode() { return getJavaScriptNode(getDomNodeOrDie().getParentNode()); } /** * Gets the JavaScript property "nextSibling" for the node that * contains the current node. * @return the next sibling node or null if the current node has * no next sibling. */ public Object jsxGet_nextSibling() { return getJavaScriptNode(getDomNodeOrDie().getNextSibling()); } /** * Gets the JavaScript property "previousSibling" for the node that * contains the current node. * @return the previous sibling node or null if the current node has * no previous sibling. */ public Object jsxGet_previousSibling() { return getJavaScriptNode(getDomNodeOrDie().getPreviousSibling()); } /** * Gets the JavaScript property "firstChild" for the node that * contains the current node. * @return the first child node or null if the current node has * no children. */ public Object jsxGet_firstChild() { return getJavaScriptNode(getDomNodeOrDie().getFirstChild()); } /** * Gets the JavaScript property "lastChild" for the node that * contains the current node. * @return the last child node or null if the current node has * no children. */ public Object jsxGet_lastChild() { return getJavaScriptNode(getDomNodeOrDie().getLastChild()); } /** * Gets the JavaScript node for a given DomNode. * @param domNode the DomNode * @return the JavaScript node or null if the DomNode was null */ protected Object getJavaScriptNode(final DomNode domNode) { if (domNode == null) { return null; } return getScriptableFor(domNode); } /** * Allows the registration of event listeners on the event target. * @param type the event type to listen for (like "onclick") * @param listener the event listener * @see * MSDN documentation * @return true if the listener has been added */ public boolean jsxFunction_attachEvent(final String type, final Function listener) { return getEventListenersContainer().addEventListener(StringUtils.substring(type, 2), listener, false); } /** * Allows the registration of event listeners on the event target. * @param type the event type to listen for (like "click") * @param listener the event listener * @param useCapture If true, indicates that the user wishes to initiate capture * @see Mozilla documentation */ public void jsxFunction_addEventListener(final String type, final Function listener, final boolean useCapture) { getEventListenersContainer().addEventListener(type, listener, useCapture); } /** * Gets the container for event listeners. * @return the container (newly created if needed) */ private EventListenersContainer getEventListenersContainer() { if (eventListenersContainer_ == null) { eventListenersContainer_ = new EventListenersContainer(this); } return eventListenersContainer_; } /** * Allows the removal of event listeners on the event target. * @param type the event type to listen for (like "onclick") * @param listener the event listener * @see * MSDN documentation */ public void jsxFunction_detachEvent(final String type, final Function listener) { jsxFunction_removeEventListener(StringUtils.substring(type, 2), listener, false); } /** * Allows the removal of event listeners on the event target. * @param type the event type to listen for (like "click") * @param listener the event listener * @param useCapture If true, indicates that the user wishes to initiate capture (not yet implemented) * @see Mozilla documentation */ public void jsxFunction_removeEventListener(final String type, final Function listener, final boolean useCapture) { getEventListenersContainer().removeEventListener(type, listener, useCapture); } /** * Executes the event on this object only (needed for instance for onload on (i)frame tags). * @param event the event * @return the result */ public ScriptResult executeEvent(final Event event) { if (eventListenersContainer_ != null) { final HtmlPage page = (HtmlPage) getDomNodeOrDie().getPage(); final boolean isIE = getBrowserVersion().isIE(); final Window window = (Window) page.getEnclosingWindow().getScriptObject(); final Object[] args = new Object[] {event}; if (isIE) { window.setEvent(event); } // handlers declared as property on a node don't receive the event as argument for IE final Object[] propHandlerArgs; if (isIE) { propHandlerArgs = ArrayUtils.EMPTY_OBJECT_ARRAY; } else { propHandlerArgs = args; } try { return eventListenersContainer_.executeListeners(event, args, propHandlerArgs); } finally { window.setEvent(null); // reset event } } return null; } /** * Fires the event on the node with capturing and bubbling phase. * @param event the event * @return the result */ public ScriptResult fireEvent(final Event event) { final HtmlPage page = (HtmlPage) getDomNodeOrDie().getPage(); final boolean ie = getBrowserVersion().isIE(); final Window window = (Window) page.getEnclosingWindow().getScriptObject(); final Object[] args = new Object[] {event}; event.startFire(); ScriptResult result = null; final Object previousEvent = window.jsxGet_event(); if (ie) { window.setEvent(event); } try { // window's listeners final EventListenersContainer windowsListeners = getWindow().getEventListenersContainer(); // capturing phase event.setEventPhase(Event.CAPTURING_PHASE); result = windowsListeners.executeCapturingListeners(event, args); if (event.isPropagationStopped()) { return result; } final List parents = new ArrayList(); DomNode node = getDomNodeOrDie(); while (node != null) { parents.add(node); node = node.getParentNode(); } for (int i = parents.size() - 1; i >= 0; i--) { final DomNode curNode = parents.get(i); final Node jsNode = (Node) curNode.getScriptObject(); final EventListenersContainer elc = jsNode.eventListenersContainer_; if (elc != null) { final ScriptResult r = elc.executeCapturingListeners(event, args); result = ScriptResult.combine(r, result, ie); if (event.isPropagationStopped()) { return result; } } } // handlers declared as property on a node don't receive the event as argument for IE final Object[] propHandlerArgs; if (ie) { propHandlerArgs = ArrayUtils.EMPTY_OBJECT_ARRAY; } else { propHandlerArgs = args; } // bubbling phase event.setEventPhase(Event.AT_TARGET); node = getDomNodeOrDie(); while (node != null) { final Node jsNode = (Node) node.getScriptObject(); final EventListenersContainer elc = jsNode.eventListenersContainer_; if (elc != null) { final ScriptResult r = elc.executeBubblingListeners(event, args, propHandlerArgs); result = ScriptResult.combine(r, result, ie); if (event.isPropagationStopped()) { return result; } } node = node.getParentNode(); event.setEventPhase(Event.BUBBLING_PHASE); } final ScriptResult r = windowsListeners.executeBubblingListeners(event, args, propHandlerArgs); result = ScriptResult.combine(r, result, ie); } finally { event.endFire(); window.setEvent(previousEvent); // reset event } return result; } /** * Gets an event handler. * @param eventName the event name (ex: "onclick") * @return the handler function, null if the property is null or not a function */ public Function getEventHandler(final String eventName) { if (eventListenersContainer_ == null) { return null; } return eventListenersContainer_.getEventHandler(StringUtils.substring(eventName, 2)); } /** * Defines an event handler. * @param eventName the event name (like "onclick") * @param eventHandler the handler (null to reset it) */ public void setEventHandler(final String eventName, final Function eventHandler) { setEventHandlerProp(eventName, eventHandler); } /** * Defines an event handler (or maybe any other object). * @param eventName the event name (like "onclick") * @param value the property (null to reset it) */ protected void setEventHandlerProp(final String eventName, final Object value) { getEventListenersContainer().setEventHandlerProp(StringUtils.substring(eventName.toLowerCase(), 2), value); } /** * Gets the property defined as event handler (not necessary a Function if something else has been set). * @param eventName the event name (like "onclick") * @return the property */ protected Object getEventHandlerProp(final String eventName) { if (eventListenersContainer_ == null) { return null; } else { return eventListenersContainer_.getEventHandlerProp(StringUtils.substring(eventName.toLowerCase(), 2)); } } /** * Returns the owner document. * @return the document */ public Object jsxGet_ownerDocument() { final Object document = getDomNodeOrDie().getOwnerDocument(); if (document == null) { return null; } return ((SgmlPage) document).getScriptObject(); } }