|
1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 const {Cu, Ci} = require("chrome"); |
|
10 let EventEmitter = require("devtools/toolkit/event-emitter"); |
|
11 |
|
12 /** |
|
13 * API |
|
14 * |
|
15 * new Selection(walker=null, node=null, track={attributes,detached}); |
|
16 * destroy() |
|
17 * node (readonly) |
|
18 * setNode(node, origin="unknown") |
|
19 * |
|
20 * Helpers: |
|
21 * |
|
22 * window |
|
23 * document |
|
24 * isRoot() |
|
25 * isNode() |
|
26 * isHTMLNode() |
|
27 * |
|
28 * Check the nature of the node: |
|
29 * |
|
30 * isElementNode() |
|
31 * isAttributeNode() |
|
32 * isTextNode() |
|
33 * isCDATANode() |
|
34 * isEntityRefNode() |
|
35 * isEntityNode() |
|
36 * isProcessingInstructionNode() |
|
37 * isCommentNode() |
|
38 * isDocumentNode() |
|
39 * isDocumentTypeNode() |
|
40 * isDocumentFragmentNode() |
|
41 * isNotationNode() |
|
42 * |
|
43 * Events: |
|
44 * "new-node" when the inner node changed |
|
45 * "before-new-node" when the inner node is set to change |
|
46 * "attribute-changed" when an attribute is changed (only if tracked) |
|
47 * "detached" when the node (or one of its parents) is removed from the document (only if tracked) |
|
48 * "reparented" when the node (or one of its parents) is moved under a different node (only if tracked) |
|
49 */ |
|
50 |
|
51 /** |
|
52 * A Selection object. Hold a reference to a node. |
|
53 * Includes some helpers, fire some helpful events. |
|
54 * |
|
55 * @param node Inner node. |
|
56 * Can be null. Can be (un)set in the future via the "node" property; |
|
57 * @param trackAttribute Tell if events should be fired when the attributes of |
|
58 * the node change. |
|
59 * |
|
60 */ |
|
61 function Selection(walker, node=null, track={attributes:true,detached:true}) { |
|
62 EventEmitter.decorate(this); |
|
63 |
|
64 this._onMutations = this._onMutations.bind(this); |
|
65 this.track = track; |
|
66 this.setWalker(walker); |
|
67 this.setNode(node); |
|
68 } |
|
69 |
|
70 exports.Selection = Selection; |
|
71 |
|
72 Selection.prototype = { |
|
73 _walker: null, |
|
74 _node: null, |
|
75 |
|
76 _onMutations: function(mutations) { |
|
77 let attributeChange = false; |
|
78 let pseudoChange = false; |
|
79 let detached = false; |
|
80 let parentNode = null; |
|
81 |
|
82 for (let m of mutations) { |
|
83 if (!attributeChange && m.type == "attributes") { |
|
84 attributeChange = true; |
|
85 } |
|
86 if (m.type == "childList") { |
|
87 if (!detached && !this.isConnected()) { |
|
88 if (this.isNode()) { |
|
89 parentNode = m.target; |
|
90 } |
|
91 detached = true; |
|
92 } |
|
93 } |
|
94 if (m.type == "pseudoClassLock") { |
|
95 pseudoChange = true; |
|
96 } |
|
97 } |
|
98 |
|
99 // Fire our events depending on what changed in the mutations array |
|
100 if (attributeChange) { |
|
101 this.emit("attribute-changed"); |
|
102 } |
|
103 if (pseudoChange) { |
|
104 this.emit("pseudoclass"); |
|
105 } |
|
106 if (detached) { |
|
107 let rawNode = null; |
|
108 if (parentNode && parentNode.isLocal_toBeDeprecated()) { |
|
109 rawNode = parentNode.rawNode(); |
|
110 } |
|
111 |
|
112 this.emit("detached", rawNode, null); |
|
113 this.emit("detached-front", parentNode); |
|
114 } |
|
115 }, |
|
116 |
|
117 destroy: function() { |
|
118 this.setNode(null); |
|
119 this.setWalker(null); |
|
120 }, |
|
121 |
|
122 setWalker: function(walker) { |
|
123 if (this._walker) { |
|
124 this._walker.off("mutations", this._onMutations); |
|
125 } |
|
126 this._walker = walker; |
|
127 if (this._walker) { |
|
128 this._walker.on("mutations", this._onMutations); |
|
129 } |
|
130 }, |
|
131 |
|
132 // Not remote-safe |
|
133 setNode: function(value, reason="unknown") { |
|
134 if (value) { |
|
135 value = this._walker.frontForRawNode(value); |
|
136 } |
|
137 this.setNodeFront(value, reason); |
|
138 }, |
|
139 |
|
140 // Not remote-safe |
|
141 get node() { |
|
142 return this._node; |
|
143 }, |
|
144 |
|
145 // Not remote-safe |
|
146 get window() { |
|
147 if (this.isNode()) { |
|
148 return this.node.ownerDocument.defaultView; |
|
149 } |
|
150 return null; |
|
151 }, |
|
152 |
|
153 // Not remote-safe |
|
154 get document() { |
|
155 if (this.isNode()) { |
|
156 return this.node.ownerDocument; |
|
157 } |
|
158 return null; |
|
159 }, |
|
160 |
|
161 setNodeFront: function(value, reason="unknown") { |
|
162 this.reason = reason; |
|
163 |
|
164 // We used to return here if the node had not changed but we now need to |
|
165 // set the node even if it is already set otherwise it is not possible to |
|
166 // e.g. highlight the same node twice. |
|
167 let rawValue = null; |
|
168 if (value && value.isLocal_toBeDeprecated()) { |
|
169 rawValue = value.rawNode(); |
|
170 } |
|
171 this.emit("before-new-node", rawValue, reason); |
|
172 this.emit("before-new-node-front", value, reason); |
|
173 let previousNode = this._node; |
|
174 let previousFront = this._nodeFront; |
|
175 this._node = rawValue; |
|
176 this._nodeFront = value; |
|
177 this.emit("new-node", previousNode, this.reason); |
|
178 this.emit("new-node-front", value, this.reason); |
|
179 }, |
|
180 |
|
181 get documentFront() { |
|
182 return this._walker.document(this._nodeFront); |
|
183 }, |
|
184 |
|
185 get nodeFront() { |
|
186 return this._nodeFront; |
|
187 }, |
|
188 |
|
189 isRoot: function() { |
|
190 return this.isNode() && |
|
191 this.isConnected() && |
|
192 this._nodeFront.isDocumentElement; |
|
193 }, |
|
194 |
|
195 isNode: function() { |
|
196 if (!this._nodeFront) { |
|
197 return false; |
|
198 } |
|
199 |
|
200 // As long as tools are still accessing node.rawNode(), |
|
201 // this needs to stay here. |
|
202 if (this._node && Cu.isDeadWrapper(this._node)) { |
|
203 return false; |
|
204 } |
|
205 |
|
206 return true; |
|
207 }, |
|
208 |
|
209 isLocal: function() { |
|
210 return !!this._node; |
|
211 }, |
|
212 |
|
213 isConnected: function() { |
|
214 let node = this._nodeFront; |
|
215 if (!node || !node.actorID) { |
|
216 return false; |
|
217 } |
|
218 |
|
219 // As long as there are still tools going around |
|
220 // accessing node.rawNode, this needs to stay. |
|
221 let rawNode = null; |
|
222 if (node.isLocal_toBeDeprecated()) { |
|
223 rawNode = node.rawNode(); |
|
224 } |
|
225 if (rawNode) { |
|
226 try { |
|
227 let doc = this.document; |
|
228 return (doc && doc.defaultView && doc.documentElement.contains(rawNode)); |
|
229 } catch (e) { |
|
230 // "can't access dead object" error |
|
231 return false; |
|
232 } |
|
233 } |
|
234 |
|
235 while(node) { |
|
236 if (node === this._walker.rootNode) { |
|
237 return true; |
|
238 } |
|
239 node = node.parentNode(); |
|
240 }; |
|
241 return false; |
|
242 }, |
|
243 |
|
244 isHTMLNode: function() { |
|
245 let xhtml_ns = "http://www.w3.org/1999/xhtml"; |
|
246 return this.isNode() && this.node.namespaceURI == xhtml_ns; |
|
247 }, |
|
248 |
|
249 // Node type |
|
250 |
|
251 isElementNode: function() { |
|
252 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE; |
|
253 }, |
|
254 |
|
255 isAttributeNode: function() { |
|
256 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ATTRIBUTE_NODE; |
|
257 }, |
|
258 |
|
259 isTextNode: function() { |
|
260 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.TEXT_NODE; |
|
261 }, |
|
262 |
|
263 isCDATANode: function() { |
|
264 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.CDATA_SECTION_NODE; |
|
265 }, |
|
266 |
|
267 isEntityRefNode: function() { |
|
268 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_REFERENCE_NODE; |
|
269 }, |
|
270 |
|
271 isEntityNode: function() { |
|
272 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_NODE; |
|
273 }, |
|
274 |
|
275 isProcessingInstructionNode: function() { |
|
276 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; |
|
277 }, |
|
278 |
|
279 isCommentNode: function() { |
|
280 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; |
|
281 }, |
|
282 |
|
283 isDocumentNode: function() { |
|
284 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE; |
|
285 }, |
|
286 |
|
287 isDocumentTypeNode: function() { |
|
288 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE; |
|
289 }, |
|
290 |
|
291 isDocumentFragmentNode: function() { |
|
292 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE; |
|
293 }, |
|
294 |
|
295 isNotationNode: function() { |
|
296 return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.NOTATION_NODE; |
|
297 }, |
|
298 }; |