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/. */
7 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
9 loader.lazyRequireGetter(
12 "resource://devtools/shared/dom-node-constants.js"
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
24 * nodeFront (readonly)
25 * setNodeFront(node, origin="unknown")
35 * Check the nature of the node:
43 * isProcessingInstructionNode()
46 * isDocumentTypeNode()
47 * isDocumentFragmentNode()
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
55 * "reparented" when the node (or one of its parents) is moved under
58 function Selection() {
59 EventEmitter.decorate(this);
61 // The WalkerFront is dynamic and is always set to the selected NodeFront's WalkerFront.
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;
77 let parentNode = null;
79 for (const m of mutations) {
80 if (!attributeChange && m.type == "attributes") {
81 attributeChange = true;
83 if (m.type == "childList") {
84 if (!detached && !this.isConnected()) {
86 parentNode = m.target;
91 if (m.type == "pseudoClassLock") {
96 // Fire our events depending on what changed in the mutations array
97 if (attributeChange) {
98 this.emit("attribute-changed");
101 this.emit("pseudoclass");
104 this.emit("detached-front", parentNode);
110 this._nodeFront = null;
114 * @param {WalkerFront|null} walker
116 setWalker(walker = null) {
118 this._removeWalkerFrontEventListeners(this._walker);
121 this._walker = walker;
123 this._setWalkerFrontEventListeners(this._walker);
128 * Set event listeners on the passed walker front
130 * @param {WalkerFront} walker
132 _setWalkerFrontEventListeners(walker) {
133 walker.on("mutations", this._onMutations);
137 * Remove event listeners we previously set on walker front
139 * @param {WalkerFront} walker
141 _removeWalkerFrontEventListeners(walker) {
142 walker.off("mutations", this._onMutations);
146 * Called when a target front is destroyed.
148 * @param {TargetFront} front
149 * @emits detached-front
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
157 !targetFront.isTopLevel &&
158 this._walker.targetFront == targetFront
160 this._removeWalkerFrontEventListeners(this._walker);
161 this.emit("detached-front");
166 * Update the currently selected node-front.
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
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;
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).
192 this.emit("node-front-will-unset");
194 this._isSlotted = isSlotted;
195 this._nodeFront = nodeFront;
198 this.setWalker(nodeFront.walkerFront);
203 this.emit("new-node-front", nodeFront, this.reason);
207 return this._nodeFront;
212 this.isNode() && this.isConnected() && this._nodeFront.isDocumentElement
217 return !!this._nodeFront;
221 let node = this._nodeFront;
222 if (!node || node.isDestroyed()) {
227 if (node === this._walker.rootNode) {
230 node = node.parentOrHost();
236 const xhtmlNs = "http://www.w3.org/1999/xhtml";
237 return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
241 const svgNs = "http://www.w3.org/2000/svg";
242 return this.isNode() && this.nodeFront.namespaceURI == svgNs;
246 const mathmlNs = "http://www.w3.org/1998/Math/MathML";
247 return this.isNode() && this.nodeFront.namespaceURI == mathmlNs;
254 this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE
258 isPseudoElementNode() {
259 return this.isNode() && this.nodeFront.isPseudoElement;
263 return this.isNode() && this.nodeFront.isAnonymous;
268 this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE
273 return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
279 this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE
286 this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE
292 this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE
296 isProcessingInstructionNode() {
299 this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
306 this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
312 this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE
317 * @returns true if the selection is the <body> HTML element.
322 this.isConnected() &&
323 this.nodeFront.nodeName === "BODY"
328 * @returns true if the selection is the <head> HTML element.
333 this.isConnected() &&
334 this.nodeFront.nodeName === "HEAD"
338 isDocumentTypeNode() {
341 this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE
345 isDocumentFragmentNode() {
348 this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE
354 this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE
359 return this._isSlotted;
363 return this.isNode() && this.nodeFront.isShadowRoot;
367 module.exports = Selection;