001: /*
002: * Copyright (c) 2002-2008 Gargoyle Software Inc. All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * 1. Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: * 2. Redistributions in binary form must reproduce the above copyright notice,
010: * this list of conditions and the following disclaimer in the documentation
011: * and/or other materials provided with the distribution.
012: * 3. The end-user documentation included with the redistribution, if any, must
013: * include the following acknowledgment:
014: *
015: * "This product includes software developed by Gargoyle Software Inc.
016: * (http://www.GargoyleSoftware.com/)."
017: *
018: * Alternately, this acknowledgment may appear in the software itself, if
019: * and wherever such third-party acknowledgments normally appear.
020: * 4. The name "Gargoyle Software" must not be used to endorse or promote
021: * products derived from this software without prior written permission.
022: * For written permission, please contact info@GargoyleSoftware.com.
023: * 5. Products derived from this software may not be called "HtmlUnit", nor may
024: * "HtmlUnit" appear in their name, without prior written permission of
025: * Gargoyle Software Inc.
026: *
027: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
028: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
029: * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARGOYLE
030: * SOFTWARE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
031: * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
032: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
033: * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
034: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
035: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
036: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037: */
038: package com.gargoylesoftware.htmlunit.javascript.host;
039:
040: import java.util.ArrayList;
041: import java.util.List;
042:
043: import org.apache.commons.lang.ArrayUtils;
044: import org.apache.commons.lang.StringUtils;
045: import org.jaxen.JaxenException;
046: import org.mozilla.javascript.Context;
047: import org.mozilla.javascript.Function;
048:
049: import com.gargoylesoftware.htmlunit.ScriptResult;
050: import com.gargoylesoftware.htmlunit.SgmlPage;
051: import com.gargoylesoftware.htmlunit.html.DomDocumentFragment;
052: import com.gargoylesoftware.htmlunit.html.DomNode;
053: import com.gargoylesoftware.htmlunit.html.HtmlElement;
054: import com.gargoylesoftware.htmlunit.html.HtmlPage;
055: import com.gargoylesoftware.htmlunit.html.xpath.HtmlUnitXPath;
056: import com.gargoylesoftware.htmlunit.javascript.HTMLCollection;
057: import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
058:
059: /**
060: * The javascript object "Node" which is the base class for all DOM
061: * objects. This will typically wrap an instance of {@link DomNode}.
062: *
063: * @version $Revision: 2157 $
064: * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
065: * @author David K. Taylor
066: * @author Barnaby Court
067: * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
068: * @author <a href="mailto:george@murnock.com">George Murnock</a>
069: * @author Chris Erskine
070: * @author Bruce Faulkner
071: * @author Ahmed Ashour
072: */
073: public class Node extends SimpleScriptable {
074:
075: private HTMLCollection childNodes_; //has to be a member to have equality (==) working
076: private static final long serialVersionUID = -5695262053081637445L;
077: private EventListenersContainer eventListenersContainer_;
078:
079: /**
080: * @see {@link org.w3c.dom.Node#ELEMENT_NODE}.
081: */
082: public static final short ELEMENT_NODE = org.w3c.dom.Node.ELEMENT_NODE;
083: /**
084: * @see {@link org.w3c.dom.Node#ATTRIBUTE_NODE}.
085: */
086: public static final short ATTRIBUTE_NODE = org.w3c.dom.Node.ATTRIBUTE_NODE;
087: /**
088: * @see {@link org.w3c.dom.Node#TEXT_NODE}.
089: */
090: public static final short TEXT_NODE = org.w3c.dom.Node.TEXT_NODE;
091: /**
092: * @see {@link org.w3c.dom.Node#CDATA_SECTION_NODE}.
093: */
094: public static final short CDATA_SECTION_NODE = org.w3c.dom.Node.CDATA_SECTION_NODE;
095: /**
096: * @see {@link org.w3c.dom.Node#ENTITY_REFERENCE_NODE}.
097: */
098: public static final short ENTITY_REFERENCE_NODE = org.w3c.dom.Node.ENTITY_REFERENCE_NODE;
099: /**
100: * @see {@link org.w3c.dom.Node#ENTITY_NODE}.
101: */
102: public static final short ENTITY_NODE = org.w3c.dom.Node.ENTITY_NODE;
103: /**
104: * @see {@link org.w3c.dom.Node#PROCESSING_INSTRUCTION_NODE}.
105: */
106: public static final short PROCESSING_INSTRUCTION_NODE = org.w3c.dom.Node.PROCESSING_INSTRUCTION_NODE;
107: /**
108: * @see {@link org.w3c.dom.Node#COMMENT_NODE}.
109: */
110: public static final short COMMENT_NODE = org.w3c.dom.Node.COMMENT_NODE;
111: /**
112: * @see {@link org.w3c.dom.Node#DOCUMENT_NODE}.
113: */
114: public static final short DOCUMENT_NODE = org.w3c.dom.Node.DOCUMENT_NODE;
115: /**
116: * @see {@link org.w3c.dom.Node#DOCUMENT_TYPE_NODE}.
117: */
118: public static final short DOCUMENT_TYPE_NODE = org.w3c.dom.Node.DOCUMENT_TYPE_NODE;
119: /**
120: * @see {@link org.w3c.dom.Node#DOCUMENT_FRAGMENT_NODE}.
121: */
122: public static final short DOCUMENT_FRAGMENT_NODE = org.w3c.dom.Node.DOCUMENT_FRAGMENT_NODE;
123: /**
124: * @see {@link org.w3c.dom.Node#NOTATION_NODE}.
125: */
126: public static final short NOTATION_NODE = org.w3c.dom.Node.NOTATION_NODE;
127:
128: /**
129: * Create an instance.
130: */
131: public Node() {
132: }
133:
134: /**
135: * Get the JavaScript property "nodeType" for the current node.
136: * @return The node type
137: */
138: public short jsxGet_nodeType() {
139: return getDomNodeOrDie().getNodeType();
140: }
141:
142: /**
143: * Get the JavaScript property "nodeName" for the current node.
144: * @return The node name
145: */
146: public String jsxGet_nodeName() {
147: final DomNode domNode = getDomNodeOrDie();
148: String nodeName = domNode.getNodeName();
149:
150: // If this is an HtmlElement then flip the result to uppercase. This should really be
151: // changed in HtmlElement itself but that would break backwards compatibility fairly
152: // significantly as that one is documented as always returning a lowercase value.
153: if (domNode instanceof HtmlElement
154: && ((HtmlElement) domNode).getNamespaceURI() == null) {
155: nodeName = nodeName.toUpperCase();
156: }
157: return nodeName;
158: }
159:
160: /**
161: * Get the JavaScript property "nodeValue" for the current node.
162: * @return The node value
163: */
164: public String jsxGet_nodeValue() {
165: return getDomNodeOrDie().getNodeValue();
166: }
167:
168: /**
169: * Set the JavaScript property "nodeValue" for the current node.
170: * @param newValue The new node value
171: */
172: public void jsxSet_nodeValue(final String newValue) {
173: getDomNodeOrDie().setNodeValue(newValue);
174: }
175:
176: /**
177: * Add a DOM node to the node
178: * @param childObject The node to add to this node
179: * @return The newly added child node.
180: */
181: public Object jsxFunction_appendChild(final Object childObject) {
182: Object appendedChild = null;
183: if (childObject instanceof Node) {
184: // Get XML node for the DOM node passed in
185: final DomNode childDomNode = ((Node) childObject)
186: .getDomNodeOrDie();
187:
188: // Get the parent XML node that the child should be added to.
189: final DomNode parentNode = getDomNodeOrDie();
190:
191: // Append the child to the parent node
192: parentNode.appendDomChild(childDomNode);
193: appendedChild = childObject;
194:
195: //if the parentNode has null parentNode in IE,
196: //create a DocumentFragment to be the parentNode's parentNode.
197: if (!(this instanceof DocumentFragment)
198: && parentNode.getParentDomNode() == null
199: && getWindow().getWebWindow().getWebClient()
200: .getBrowserVersion().isIE()) {
201: final DomDocumentFragment fragment = ((SgmlPage) parentNode
202: .getNativePage()).createDomDocumentFragment();
203: fragment.appendDomChild(parentNode);
204: }
205: }
206: return appendedChild;
207: }
208:
209: /**
210: * Duplicate an XML node
211: * @param deep If true, recursively clone all descendants. Otherwise,
212: * just clone this node.
213: * @return The newly cloned node.
214: */
215: public Object jsxFunction_cloneNode(final boolean deep) {
216: final DomNode domNode = getDomNodeOrDie();
217: final DomNode clonedNode = domNode.cloneDomNode(deep);
218: return getJavaScriptNode(clonedNode);
219: }
220:
221: /**
222: * Add a DOM node as a child to this node before the referenced
223: * node. If the referenced node is null, append to the end.
224: * @param newChildObject The node to add to this node
225: * @param refChildObject The node before which to add the new child
226: * @return The newly added child node.
227: */
228: public Object jsxFunction_insertBefore(final Object newChildObject,
229: final Object refChildObject) {
230: Object appendedChild = null;
231:
232: if (newChildObject instanceof Node) {
233:
234: final DomNode newChildNode = ((Node) newChildObject)
235: .getDomNodeOrDie();
236:
237: final DomNode refChildNode;
238: // IE accepts non standard calls with only one arg
239: if (Context.getUndefinedValue().equals(refChildObject)) {
240: if (getWindow().getWebWindow().getWebClient()
241: .getBrowserVersion().isIE()) {
242: refChildNode = null;
243: } else {
244: throw Context
245: .reportRuntimeError("insertBefore: not enough arguments");
246: }
247: } else if (refChildObject != null) {
248: refChildNode = ((Node) refChildObject)
249: .getDomNodeOrDie();
250: } else {
251: refChildNode = null;
252: }
253:
254: // Append the child to the parent node
255: if (refChildNode != null) {
256: refChildNode.insertBefore(newChildNode);
257: appendedChild = newChildObject;
258: } else {
259: getDomNodeOrDie().appendDomChild(newChildNode);
260: }
261:
262: //if parentNode is null in IE, create a DocumentFragment to be the parentNode
263: if (getDomNodeOrDie().getParentDomNode() == null
264: && getWindow().getWebWindow().getWebClient()
265: .getBrowserVersion().isIE()) {
266: final DomDocumentFragment fragment = getDomNodeOrDie()
267: .getPage().createDomDocumentFragment();
268: fragment.appendDomChild(getDomNodeOrDie());
269: }
270: }
271: return appendedChild;
272: }
273:
274: /**
275: * This method provides a way to determine whether two Node references returned by
276: * the implementation reference the same object.
277: * When two Node references are references to the same object, even if through a proxy,
278: * the references may be used completely interchangeably, such that all attributes
279: * have the same values and calling the same DOM method on either reference always has exactly the same effect.
280: *
281: * @param other The node to test against.
282: *
283: * @return whether this node is the same node as the given one.
284: */
285: public boolean jsxFunction_isSameNode(final Object other) {
286: return other == this ;
287: }
288:
289: /**
290: * Remove a DOM node from this node
291: * @param childObject The node to remove from this node
292: * @return The removed child node.
293: */
294: public Object jsxFunction_removeChild(final Object childObject) {
295: Object removedChild = null;
296:
297: if (childObject instanceof Node) {
298: // Get XML node for the DOM node passed in
299: final DomNode childNode = ((Node) childObject)
300: .getDomNodeOrDie();
301:
302: // Remove the child from the parent node
303: childNode.remove();
304: removedChild = childObject;
305: }
306: return removedChild;
307: }
308:
309: /**
310: * Returns whether this node has any children.
311: * @return boolean true if this node has any children, false otherwise.
312: */
313: public boolean jsxFunction_hasChildNodes() {
314: return getDomNodeOrDie().getChildIterator().hasNext();
315: }
316:
317: /**
318: * Returns the child nodes of the current element.
319: * @return The child nodes of the current element.
320: */
321: public Object jsxGet_childNodes() {
322: if (childNodes_ == null) {
323: childNodes_ = new HTMLCollection(this );
324: try {
325: childNodes_.init(getDomNodeOrDie(), new HtmlUnitXPath(
326: "(./* | text() | comment())"));
327: } catch (final JaxenException je) {
328: throw Context
329: .reportRuntimeError("Failed to initialize collection element.childNodes: "
330: + je.getMessage());
331: }
332: }
333: return childNodes_;
334: }
335:
336: /**
337: * Replaces a child DOM node with another DOM node.
338: * @param newChildObject the node to add as a child of this node
339: * @param oldChildObject the node to remove as a child of this node
340: * @return the removed child node
341: */
342: public Object jsxFunction_replaceChild(final Object newChildObject,
343: final Object oldChildObject) {
344:
345: Object removedChild = null;
346:
347: if (newChildObject instanceof Node
348: && oldChildObject instanceof Node) {
349: // Get XML nodes for the DOM nodes passed in
350: final DomNode newChildNode = ((Node) newChildObject)
351: .getDomNodeOrDie();
352: final DomNode oldChildNode;
353: if (oldChildObject != null) {
354: // Replace the old child with the new child.
355: oldChildNode = ((Node) oldChildObject)
356: .getDomNodeOrDie();
357: oldChildNode.replace(newChildNode);
358: removedChild = oldChildObject;
359: }
360: }
361:
362: return removedChild;
363: }
364:
365: /**
366: * Get the JavaScript property "parentNode" for the node that
367: * contains the current node.
368: * @return The parent node
369: */
370: public Object jsxGet_parentNode() {
371: return getJavaScriptNode(getDomNodeOrDie().getParentDomNode());
372: }
373:
374: /**
375: * Get the JavaScript property "nextSibling" for the node that
376: * contains the current node.
377: * @return The next sibling node or null if the current node has
378: * no next sibling.
379: */
380: public Object jsxGet_nextSibling() {
381: return getJavaScriptNode(getDomNodeOrDie().getNextDomSibling());
382: }
383:
384: /**
385: * Get the JavaScript property "previousSibling" for the node that
386: * contains the current node.
387: * @return The previous sibling node or null if the current node has
388: * no previous sibling.
389: */
390: public Object jsxGet_previousSibling() {
391: return getJavaScriptNode(getDomNodeOrDie()
392: .getPreviousDomSibling());
393: }
394:
395: /**
396: * Get the JavaScript property "firstChild" for the node that
397: * contains the current node.
398: * @return The first child node or null if the current node has
399: * no children.
400: */
401: public Object jsxGet_firstChild() {
402: return getJavaScriptNode(getDomNodeOrDie().getFirstDomChild());
403: }
404:
405: /**
406: * Get the JavaScript property "lastChild" for the node that
407: * contains the current node.
408: * @return The last child node or null if the current node has
409: * no children.
410: */
411: public Object jsxGet_lastChild() {
412: return getJavaScriptNode(getDomNodeOrDie().getLastDomChild());
413: }
414:
415: /**
416: * Get the JavaScript node for a given DomNode
417: * @param domNode The DomNode
418: * @return The JavaScript node or null if the DomNode was null.
419: */
420: protected Object getJavaScriptNode(final DomNode domNode) {
421: if (domNode == null) {
422: return null;
423: }
424: return getScriptableFor(domNode);
425: }
426:
427: /**
428: * Allows the registration of event listeners on the event target.
429: * @param type the event type to listen for (like "onclick")
430: * @param listener the event listener
431: * @see <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/attachevent.asp">
432: * MSDN documentation</a>
433: * @return <code>true</code> if the listener has been added
434: */
435: public boolean jsxFunction_attachEvent(final String type,
436: final Function listener) {
437: return getEventListenersContainer().addEventListener(
438: StringUtils.substring(type, 2), listener, false);
439: }
440:
441: /**
442: * Allows the registration of event listeners on the event target.
443: * @param type the event type to listen for (like "click")
444: * @param listener the event listener
445: * @param useCapture If <code>true</code>, indicates that the user wishes to initiate capture
446: * @see <a href="http://developer.mozilla.org/en/docs/DOM:element.addEventListener">Mozilla documentation</a>
447: */
448: public void jsxFunction_addEventListener(final String type,
449: final Function listener, final boolean useCapture) {
450: getEventListenersContainer().addEventListener(type, listener,
451: useCapture);
452: }
453:
454: /**
455: * Gets the container for event listeners
456: * @return the container (newly created if needed)
457: */
458: private EventListenersContainer getEventListenersContainer() {
459: if (eventListenersContainer_ == null) {
460: eventListenersContainer_ = new EventListenersContainer(this );
461: }
462: return eventListenersContainer_;
463: }
464:
465: /**
466: * Allows the removal of event listeners on the event target.
467: * @param type the event type to listen for (like "onclick")
468: * @param listener the event listener
469: * @see <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/detachevent.asp">
470: * MSDN documentation</a>
471: */
472: public void jsxFunction_detachEvent(final String type,
473: final Function listener) {
474: jsxFunction_removeEventListener(StringUtils.substring(type, 2),
475: listener, false);
476: }
477:
478: /**
479: * Allows the removal of event listeners on the event target.
480: * @param type the event type to listen for (like "click")
481: * @param listener the event listener
482: * @param useCapture If <code>true</code>, indicates that the user wishes to initiate capture (not yet implemented)
483: * @see <a href="http://developer.mozilla.org/en/docs/DOM:element.removeEventListener">Mozilla documentation</a>
484: */
485: public void jsxFunction_removeEventListener(final String type,
486: final Function listener, final boolean useCapture) {
487: getEventListenersContainer().removeEventListener(type,
488: listener, useCapture);
489: }
490:
491: /**
492: * Execute the event on this object only (needed for instance for onload on (i)frame tags)
493: * @param event the event
494: * @return the result
495: */
496: public ScriptResult executeEvent(final Event event) {
497: if (eventListenersContainer_ != null) {
498: final HtmlPage page = getDomNodeOrDie().getPage();
499: final boolean isIE = page.getWebClient()
500: .getBrowserVersion().isIE();
501: final Window window = (Window) page.getEnclosingWindow()
502: .getScriptObject();
503: final Object[] args = new Object[] { event };
504: if (isIE) {
505: window.setEvent(event);
506: }
507:
508: // handlers declared as property on a node don't receive the event as argument for IE
509: final Object[] propHandlerArgs;
510: if (isIE) {
511: propHandlerArgs = ArrayUtils.EMPTY_OBJECT_ARRAY;
512: } else {
513: propHandlerArgs = args;
514: }
515:
516: try {
517: return eventListenersContainer_.executeListeners(event,
518: args, propHandlerArgs);
519: } finally {
520: window.setEvent(null); // reset event
521: }
522: }
523:
524: return null;
525: }
526:
527: /**
528: * Fires the event on the node with capturing and bubbling phase
529: * @param event the event
530: * @return the result
531: */
532: public ScriptResult fireEvent(final Event event) {
533: final HtmlPage page = getDomNodeOrDie().getPage();
534: final boolean isIE = page.getWebClient().getBrowserVersion()
535: .isIE();
536: final Window window = (Window) page.getEnclosingWindow()
537: .getScriptObject();
538: final Object[] args = new Object[] { event };
539:
540: event.startFire();
541: ScriptResult result = null;
542: if (isIE) {
543: window.setEvent(event);
544: }
545: try {
546: // window's listeners
547: final EventListenersContainer windowsListeners = getWindow()
548: .getEventListenersContainer();
549:
550: // capturing phase
551: event.setEventPhase(Event.CAPTURING_PHASE);
552: result = windowsListeners.executeCapturingListeners(event,
553: args);
554: if (event.isPropagationStopped()) {
555: return result;
556: }
557: final List parents = new ArrayList();
558: DomNode node = getDomNodeOrDie();
559: while (node != null) {
560: parents.add(node);
561: node = node.getParentDomNode();
562: }
563: for (int i = parents.size() - 1; i >= 0; i--) {
564: final DomNode curNode = (DomNode) parents.get(i);
565: final Node jsNode = (Node) curNode.getScriptObject();
566: if (jsNode.eventListenersContainer_ != null) {
567: result = defaultResult(
568: jsNode.eventListenersContainer_
569: .executeCapturingListeners(event,
570: args), result);
571: if (event.isPropagationStopped()) {
572: return result;
573: }
574: }
575: }
576:
577: // handlers declared as property on a node don't receive the event as argument for IE
578: final Object[] propHandlerArgs;
579: if (isIE) {
580: propHandlerArgs = ArrayUtils.EMPTY_OBJECT_ARRAY;
581: } else {
582: propHandlerArgs = args;
583: }
584:
585: // bubbling phase
586: event.setEventPhase(Event.AT_TARGET);
587: node = getDomNodeOrDie();
588: while (node != null) {
589: final Node jsNode = (Node) node.getScriptObject();
590:
591: if (jsNode.eventListenersContainer_ != null) {
592: result = defaultResult(
593: jsNode.eventListenersContainer_
594: .executeBubblingListeners(event,
595: args, propHandlerArgs),
596: result);
597: if (event.isPropagationStopped()) {
598: return result;
599: }
600: }
601:
602: node = node.getParentDomNode();
603: event.setEventPhase(Event.BUBBLING_PHASE);
604: }
605:
606: result = defaultResult(windowsListeners
607: .executeBubblingListeners(event, args,
608: propHandlerArgs), result);
609: } finally {
610: event.endFire();
611: window.setEvent(null); // reset event
612: }
613:
614: return result;
615: }
616:
617: private ScriptResult defaultResult(final ScriptResult newResult,
618: final ScriptResult defaultResult) {
619: if (newResult != null) {
620: return newResult;
621: }
622: return defaultResult;
623: }
624:
625: /**
626: * Gets an event handler
627: * @param eventName the event name (ex: "onclick")
628: * @return the handler function, <code>null</code> if the property is null or not a function
629: */
630: public Function getEventHandler(final String eventName) {
631: if (eventListenersContainer_ == null) {
632: return null;
633: }
634: return eventListenersContainer_.getEventHandler(StringUtils
635: .substring(eventName, 2));
636: }
637:
638: /**
639: * Defines an event handler
640: * @param eventName the event name (like "onclick")
641: * @param eventHandler the handler (<code>null</code> to reset it)
642: */
643: public void setEventHandler(final String eventName,
644: final Function eventHandler) {
645: setEventHandlerProp(eventName, eventHandler);
646: }
647:
648: /**
649: * Defines an event handler (or maybe any other object)
650: * @param eventName the event name (like "onclick")
651: * @param value the property (<code>null</code> to reset it)
652: */
653: protected void setEventHandlerProp(final String eventName,
654: final Object value) {
655: getEventListenersContainer().setEventHandlerProp(
656: StringUtils.substring(eventName.toLowerCase(), 2),
657: value);
658: }
659:
660: /**
661: * Gets the property defined as event handler (not necessary a Function if something else has been set)
662: * @param eventName the event name (like "onclick")
663: * @return the property
664: */
665: protected Object getEventHandlerProp(final String eventName) {
666: if (eventListenersContainer_ == null) {
667: return null;
668: } else {
669: return eventListenersContainer_
670: .getEventHandlerProp(StringUtils.substring(
671: eventName.toLowerCase(), 2));
672: }
673: }
674: }
|