Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / devtools / client / framework / selection.js
bloba71bdf6b56792f805b5bb9a08ca9635ae80d39e9
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/. */
5 "use strict";
7 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
9 loader.lazyRequireGetter(
10   this,
11   "nodeConstants",
12   "resource://devtools/shared/dom-node-constants.js"
15 /**
16  * Selection is a singleton belonging to the Toolbox that manages the current selected
17  * NodeFront. In addition, it provides some helpers about the context of the selected
18  * node.
19  *
20  * API
21  *
22  *   new Selection()
23  *   destroy()
24  *   nodeFront (readonly)
25  *   setNodeFront(node, origin="unknown")
26  *
27  * Helpers:
28  *
29  *   window
30  *   document
31  *   isRoot()
32  *   isNode()
33  *   isHTMLNode()
34  *
35  * Check the nature of the node:
36  *
37  *   isElementNode()
38  *   isAttributeNode()
39  *   isTextNode()
40  *   isCDATANode()
41  *   isEntityRefNode()
42  *   isEntityNode()
43  *   isProcessingInstructionNode()
44  *   isCommentNode()
45  *   isDocumentNode()
46  *   isDocumentTypeNode()
47  *   isDocumentFragmentNode()
48  *   isNotationNode()
49  *
50  * Events:
51  *   "new-node-front" when the inner node changed
52  *   "attribute-changed" when an attribute is changed
53  *   "detached-front" when the node (or one of its parents) is removed from
54  *   the document
55  *   "reparented" when the node (or one of its parents) is moved under
56  *   a different node
57  */
58 function Selection() {
59   EventEmitter.decorate(this);
61   // The WalkerFront is dynamic and is always set to the selected NodeFront's WalkerFront.
62   this._walker = null;
63   // A single node front can be represented twice on the client when the node is a slotted
64   // element. It will be displayed once as a direct child of the host element, and once as
65   // a child of a slot in the "shadow DOM". The latter is called the slotted version.
66   this._isSlotted = false;
68   this._onMutations = this._onMutations.bind(this);
69   this.setNodeFront = this.setNodeFront.bind(this);
72 Selection.prototype = {
73   _onMutations(mutations) {
74     let attributeChange = false;
75     let pseudoChange = false;
76     let detached = false;
77     let parentNode = null;
79     for (const m of mutations) {
80       if (!attributeChange && m.type == "attributes") {
81         attributeChange = true;
82       }
83       if (m.type == "childList") {
84         if (!detached && !this.isConnected()) {
85           if (this.isNode()) {
86             parentNode = m.target;
87           }
88           detached = true;
89         }
90       }
91       if (m.type == "pseudoClassLock") {
92         pseudoChange = true;
93       }
94     }
96     // Fire our events depending on what changed in the mutations array
97     if (attributeChange) {
98       this.emit("attribute-changed");
99     }
100     if (pseudoChange) {
101       this.emit("pseudoclass");
102     }
103     if (detached) {
104       this.emit("detached-front", parentNode);
105     }
106   },
108   destroy() {
109     this.setWalker();
110     this._nodeFront = null;
111   },
113   /**
114    * @param {WalkerFront|null} walker
115    */
116   setWalker(walker = null) {
117     if (this._walker) {
118       this._removeWalkerFrontEventListeners(this._walker);
119     }
121     this._walker = walker;
122     if (this._walker) {
123       this._setWalkerFrontEventListeners(this._walker);
124     }
125   },
127   /**
128    * Set event listeners on the passed walker front
129    *
130    * @param {WalkerFront} walker
131    */
132   _setWalkerFrontEventListeners(walker) {
133     walker.on("mutations", this._onMutations);
134   },
136   /**
137    * Remove event listeners we previously set on walker front
138    *
139    * @param {WalkerFront} walker
140    */
141   _removeWalkerFrontEventListeners(walker) {
142     walker.off("mutations", this._onMutations);
143   },
145   /**
146    * Called when a target front is destroyed.
147    *
148    * @param {TargetFront} front
149    * @emits detached-front
150    */
151   onTargetDestroyed(targetFront) {
152     // if the current walker belongs to the target that is destroyed, emit a `detached-front`
153     // event so consumers can act accordingly (e.g. in the inspector, another node will be
154     // selected)
155     if (
156       this._walker &&
157       !targetFront.isTopLevel &&
158       this._walker.targetFront == targetFront
159     ) {
160       this._removeWalkerFrontEventListeners(this._walker);
161       this.emit("detached-front");
162     }
163   },
165   /**
166    * Update the currently selected node-front.
167    *
168    * @param {NodeFront} nodeFront
169    *        The NodeFront being selected.
170    * @param {Object} (optional)
171    *        - {String} reason: Reason that triggered the selection, will be fired with
172    *          the "new-node-front" event.
173    *        - {Boolean} isSlotted: Is the selection representing the slotted version of
174    *          the node.
175    */
176   setNodeFront(nodeFront, { reason = "unknown", isSlotted = false } = {}) {
177     this.reason = reason;
179     // If an inlineTextChild text node is being set, then set it's parent instead.
180     const parentNode = nodeFront && nodeFront.parentNode();
181     if (nodeFront && parentNode && parentNode.inlineTextChild === nodeFront) {
182       nodeFront = parentNode;
183     }
185     if (this._nodeFront == null && nodeFront == null) {
186       // Avoid to notify multiple "unselected" events with a null/undefined nodeFront
187       // (e.g. once when the webpage start to navigate away from the current webpage,
188       // and then again while the new page is being loaded).
189       return;
190     }
192     this.emit("node-front-will-unset");
194     this._isSlotted = isSlotted;
195     this._nodeFront = nodeFront;
197     if (nodeFront) {
198       this.setWalker(nodeFront.walkerFront);
199     } else {
200       this.setWalker();
201     }
203     this.emit("new-node-front", nodeFront, this.reason);
204   },
206   get nodeFront() {
207     return this._nodeFront;
208   },
210   isRoot() {
211     return (
212       this.isNode() && this.isConnected() && this._nodeFront.isDocumentElement
213     );
214   },
216   isNode() {
217     return !!this._nodeFront;
218   },
220   isConnected() {
221     let node = this._nodeFront;
222     if (!node || node.isDestroyed()) {
223       return false;
224     }
226     while (node) {
227       if (node === this._walker.rootNode) {
228         return true;
229       }
230       node = node.parentOrHost();
231     }
232     return false;
233   },
235   isHTMLNode() {
236     const xhtmlNs = "http://www.w3.org/1999/xhtml";
237     return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
238   },
240   isSVGNode() {
241     const svgNs = "http://www.w3.org/2000/svg";
242     return this.isNode() && this.nodeFront.namespaceURI == svgNs;
243   },
245   isMathMLNode() {
246     const mathmlNs = "http://www.w3.org/1998/Math/MathML";
247     return this.isNode() && this.nodeFront.namespaceURI == mathmlNs;
248   },
250   // Node type
252   isElementNode() {
253     return (
254       this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE
255     );
256   },
258   isPseudoElementNode() {
259     return this.isNode() && this.nodeFront.isPseudoElement;
260   },
262   isAnonymousNode() {
263     return this.isNode() && this.nodeFront.isAnonymous;
264   },
266   isAttributeNode() {
267     return (
268       this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE
269     );
270   },
272   isTextNode() {
273     return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
274   },
276   isCDATANode() {
277     return (
278       this.isNode() &&
279       this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE
280     );
281   },
283   isEntityRefNode() {
284     return (
285       this.isNode() &&
286       this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE
287     );
288   },
290   isEntityNode() {
291     return (
292       this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE
293     );
294   },
296   isProcessingInstructionNode() {
297     return (
298       this.isNode() &&
299       this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
300     );
301   },
303   isCommentNode() {
304     return (
305       this.isNode() &&
306       this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
307     );
308   },
310   isDocumentNode() {
311     return (
312       this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE
313     );
314   },
316   /**
317    * @returns true if the selection is the <body> HTML element.
318    */
319   isBodyNode() {
320     return (
321       this.isHTMLNode() &&
322       this.isConnected() &&
323       this.nodeFront.nodeName === "BODY"
324     );
325   },
327   /**
328    * @returns true if the selection is the <head> HTML element.
329    */
330   isHeadNode() {
331     return (
332       this.isHTMLNode() &&
333       this.isConnected() &&
334       this.nodeFront.nodeName === "HEAD"
335     );
336   },
338   isDocumentTypeNode() {
339     return (
340       this.isNode() &&
341       this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE
342     );
343   },
345   isDocumentFragmentNode() {
346     return (
347       this.isNode() &&
348       this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE
349     );
350   },
352   isNotationNode() {
353     return (
354       this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE
355     );
356   },
358   isSlotted() {
359     return this._isSlotted;
360   },
362   isShadowRootNode() {
363     return this.isNode() && this.nodeFront.isShadowRoot;
364   },
367 module.exports = Selection;