|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 const Ci = Components.interfaces; |
|
6 const Cu = Components.utils; |
|
7 Cu.import("resource://gre/modules/Services.jsm"); |
|
8 |
|
9 this.EXPORTED_SYMBOLS = ["DOMHelpers"]; |
|
10 |
|
11 /** |
|
12 * DOMHelpers |
|
13 * Makes DOM traversal easier. Goes through iframes. |
|
14 * |
|
15 * @constructor |
|
16 * @param nsIDOMWindow aWindow |
|
17 * The content window, owning the document to traverse. |
|
18 */ |
|
19 this.DOMHelpers = function DOMHelpers(aWindow) { |
|
20 if (!aWindow) { |
|
21 throw new Error("window can't be null or undefined"); |
|
22 } |
|
23 this.window = aWindow; |
|
24 }; |
|
25 |
|
26 DOMHelpers.prototype = { |
|
27 getParentObject: function Helpers_getParentObject(node) |
|
28 { |
|
29 let parentNode = node ? node.parentNode : null; |
|
30 |
|
31 if (!parentNode) { |
|
32 // Documents have no parentNode; Attr, Document, DocumentFragment, Entity, |
|
33 // and Notation. top level windows have no parentNode |
|
34 if (node && node == this.window.Node.DOCUMENT_NODE) { |
|
35 // document type |
|
36 if (node.defaultView) { |
|
37 let embeddingFrame = node.defaultView.frameElement; |
|
38 if (embeddingFrame) |
|
39 return embeddingFrame.parentNode; |
|
40 } |
|
41 } |
|
42 // a Document object without a parentNode or window |
|
43 return null; // top level has no parent |
|
44 } |
|
45 |
|
46 if (parentNode.nodeType == this.window.Node.DOCUMENT_NODE) { |
|
47 if (parentNode.defaultView) { |
|
48 return parentNode.defaultView.frameElement; |
|
49 } |
|
50 // parent is document element, but no window at defaultView. |
|
51 return null; |
|
52 } |
|
53 |
|
54 if (!parentNode.localName) |
|
55 return null; |
|
56 |
|
57 return parentNode; |
|
58 }, |
|
59 |
|
60 getChildObject: function Helpers_getChildObject(node, index, previousSibling, |
|
61 showTextNodesWithWhitespace) |
|
62 { |
|
63 if (!node) |
|
64 return null; |
|
65 |
|
66 if (node.contentDocument) { |
|
67 // then the node is a frame |
|
68 if (index == 0) { |
|
69 return node.contentDocument.documentElement; // the node's HTMLElement |
|
70 } |
|
71 return null; |
|
72 } |
|
73 |
|
74 if (node.getSVGDocument) { |
|
75 let svgDocument = node.getSVGDocument(); |
|
76 if (svgDocument) { |
|
77 // then the node is a frame |
|
78 if (index == 0) { |
|
79 return svgDocument.documentElement; // the node's SVGElement |
|
80 } |
|
81 return null; |
|
82 } |
|
83 } |
|
84 |
|
85 let child = null; |
|
86 if (previousSibling) // then we are walking |
|
87 child = this.getNextSibling(previousSibling); |
|
88 else |
|
89 child = this.getFirstChild(node); |
|
90 |
|
91 if (showTextNodesWithWhitespace) |
|
92 return child; |
|
93 |
|
94 for (; child; child = this.getNextSibling(child)) { |
|
95 if (!this.isWhitespaceText(child)) |
|
96 return child; |
|
97 } |
|
98 |
|
99 return null; // we have no children worth showing. |
|
100 }, |
|
101 |
|
102 getFirstChild: function Helpers_getFirstChild(node) |
|
103 { |
|
104 let SHOW_ALL = Components.interfaces.nsIDOMNodeFilter.SHOW_ALL; |
|
105 this.treeWalker = node.ownerDocument.createTreeWalker(node, |
|
106 SHOW_ALL, null); |
|
107 return this.treeWalker.firstChild(); |
|
108 }, |
|
109 |
|
110 getNextSibling: function Helpers_getNextSibling(node) |
|
111 { |
|
112 let next = this.treeWalker.nextSibling(); |
|
113 |
|
114 if (!next) |
|
115 delete this.treeWalker; |
|
116 |
|
117 return next; |
|
118 }, |
|
119 |
|
120 isWhitespaceText: function Helpers_isWhitespaceText(node) |
|
121 { |
|
122 return node.nodeType == this.window.Node.TEXT_NODE && |
|
123 !/[^\s]/.exec(node.nodeValue); |
|
124 }, |
|
125 |
|
126 destroy: function Helpers_destroy() |
|
127 { |
|
128 delete this.window; |
|
129 delete this.treeWalker; |
|
130 }, |
|
131 |
|
132 /** |
|
133 * A simple way to be notified (once) when a window becomes |
|
134 * interactive (DOMContentLoaded). |
|
135 * |
|
136 * It is based on the chromeEventHandler. This is useful when |
|
137 * chrome iframes are loaded in content docshells (in Firefox |
|
138 * tabs for example). |
|
139 */ |
|
140 onceDOMReady: function Helpers_onLocationChange(callback) { |
|
141 let window = this.window; |
|
142 let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) |
|
143 .getInterface(Ci.nsIWebNavigation) |
|
144 .QueryInterface(Ci.nsIDocShell); |
|
145 let onReady = function(event) { |
|
146 if (event.target == window.document) { |
|
147 docShell.chromeEventHandler.removeEventListener("DOMContentLoaded", onReady, false); |
|
148 // If in `callback` the URL of the window is changed and a listener to DOMContentLoaded |
|
149 // is attached, the event we just received will be also be caught by the new listener. |
|
150 // We want to avoid that so we execute the callback in the next queue. |
|
151 Services.tm.mainThread.dispatch(callback, 0); |
|
152 } |
|
153 } |
|
154 docShell.chromeEventHandler.addEventListener("DOMContentLoaded", onReady, false); |
|
155 } |
|
156 }; |