summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/selection.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/selection.js')
-rw-r--r--devtools/client/framework/selection.js367
1 files changed, 367 insertions, 0 deletions
diff --git a/devtools/client/framework/selection.js b/devtools/client/framework/selection.js
new file mode 100644
index 0000000000..a71bdf6b56
--- /dev/null
+++ b/devtools/client/framework/selection.js
@@ -0,0 +1,367 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+loader.lazyRequireGetter(
+ this,
+ "nodeConstants",
+ "resource://devtools/shared/dom-node-constants.js"
+);
+
+/**
+ * Selection is a singleton belonging to the Toolbox that manages the current selected
+ * NodeFront. In addition, it provides some helpers about the context of the selected
+ * node.
+ *
+ * API
+ *
+ * new Selection()
+ * destroy()
+ * nodeFront (readonly)
+ * setNodeFront(node, origin="unknown")
+ *
+ * Helpers:
+ *
+ * window
+ * document
+ * isRoot()
+ * isNode()
+ * isHTMLNode()
+ *
+ * Check the nature of the node:
+ *
+ * isElementNode()
+ * isAttributeNode()
+ * isTextNode()
+ * isCDATANode()
+ * isEntityRefNode()
+ * isEntityNode()
+ * isProcessingInstructionNode()
+ * isCommentNode()
+ * isDocumentNode()
+ * isDocumentTypeNode()
+ * isDocumentFragmentNode()
+ * isNotationNode()
+ *
+ * Events:
+ * "new-node-front" when the inner node changed
+ * "attribute-changed" when an attribute is changed
+ * "detached-front" when the node (or one of its parents) is removed from
+ * the document
+ * "reparented" when the node (or one of its parents) is moved under
+ * a different node
+ */
+function Selection() {
+ EventEmitter.decorate(this);
+
+ // The WalkerFront is dynamic and is always set to the selected NodeFront's WalkerFront.
+ this._walker = null;
+ // A single node front can be represented twice on the client when the node is a slotted
+ // element. It will be displayed once as a direct child of the host element, and once as
+ // a child of a slot in the "shadow DOM". The latter is called the slotted version.
+ this._isSlotted = false;
+
+ this._onMutations = this._onMutations.bind(this);
+ this.setNodeFront = this.setNodeFront.bind(this);
+}
+
+Selection.prototype = {
+ _onMutations(mutations) {
+ let attributeChange = false;
+ let pseudoChange = false;
+ let detached = false;
+ let parentNode = null;
+
+ for (const m of mutations) {
+ if (!attributeChange && m.type == "attributes") {
+ attributeChange = true;
+ }
+ if (m.type == "childList") {
+ if (!detached && !this.isConnected()) {
+ if (this.isNode()) {
+ parentNode = m.target;
+ }
+ detached = true;
+ }
+ }
+ if (m.type == "pseudoClassLock") {
+ pseudoChange = true;
+ }
+ }
+
+ // Fire our events depending on what changed in the mutations array
+ if (attributeChange) {
+ this.emit("attribute-changed");
+ }
+ if (pseudoChange) {
+ this.emit("pseudoclass");
+ }
+ if (detached) {
+ this.emit("detached-front", parentNode);
+ }
+ },
+
+ destroy() {
+ this.setWalker();
+ this._nodeFront = null;
+ },
+
+ /**
+ * @param {WalkerFront|null} walker
+ */
+ setWalker(walker = null) {
+ if (this._walker) {
+ this._removeWalkerFrontEventListeners(this._walker);
+ }
+
+ this._walker = walker;
+ if (this._walker) {
+ this._setWalkerFrontEventListeners(this._walker);
+ }
+ },
+
+ /**
+ * Set event listeners on the passed walker front
+ *
+ * @param {WalkerFront} walker
+ */
+ _setWalkerFrontEventListeners(walker) {
+ walker.on("mutations", this._onMutations);
+ },
+
+ /**
+ * Remove event listeners we previously set on walker front
+ *
+ * @param {WalkerFront} walker
+ */
+ _removeWalkerFrontEventListeners(walker) {
+ walker.off("mutations", this._onMutations);
+ },
+
+ /**
+ * Called when a target front is destroyed.
+ *
+ * @param {TargetFront} front
+ * @emits detached-front
+ */
+ onTargetDestroyed(targetFront) {
+ // if the current walker belongs to the target that is destroyed, emit a `detached-front`
+ // event so consumers can act accordingly (e.g. in the inspector, another node will be
+ // selected)
+ if (
+ this._walker &&
+ !targetFront.isTopLevel &&
+ this._walker.targetFront == targetFront
+ ) {
+ this._removeWalkerFrontEventListeners(this._walker);
+ this.emit("detached-front");
+ }
+ },
+
+ /**
+ * Update the currently selected node-front.
+ *
+ * @param {NodeFront} nodeFront
+ * The NodeFront being selected.
+ * @param {Object} (optional)
+ * - {String} reason: Reason that triggered the selection, will be fired with
+ * the "new-node-front" event.
+ * - {Boolean} isSlotted: Is the selection representing the slotted version of
+ * the node.
+ */
+ setNodeFront(nodeFront, { reason = "unknown", isSlotted = false } = {}) {
+ this.reason = reason;
+
+ // If an inlineTextChild text node is being set, then set it's parent instead.
+ const parentNode = nodeFront && nodeFront.parentNode();
+ if (nodeFront && parentNode && parentNode.inlineTextChild === nodeFront) {
+ nodeFront = parentNode;
+ }
+
+ if (this._nodeFront == null && nodeFront == null) {
+ // Avoid to notify multiple "unselected" events with a null/undefined nodeFront
+ // (e.g. once when the webpage start to navigate away from the current webpage,
+ // and then again while the new page is being loaded).
+ return;
+ }
+
+ this.emit("node-front-will-unset");
+
+ this._isSlotted = isSlotted;
+ this._nodeFront = nodeFront;
+
+ if (nodeFront) {
+ this.setWalker(nodeFront.walkerFront);
+ } else {
+ this.setWalker();
+ }
+
+ this.emit("new-node-front", nodeFront, this.reason);
+ },
+
+ get nodeFront() {
+ return this._nodeFront;
+ },
+
+ isRoot() {
+ return (
+ this.isNode() && this.isConnected() && this._nodeFront.isDocumentElement
+ );
+ },
+
+ isNode() {
+ return !!this._nodeFront;
+ },
+
+ isConnected() {
+ let node = this._nodeFront;
+ if (!node || node.isDestroyed()) {
+ return false;
+ }
+
+ while (node) {
+ if (node === this._walker.rootNode) {
+ return true;
+ }
+ node = node.parentOrHost();
+ }
+ return false;
+ },
+
+ isHTMLNode() {
+ const xhtmlNs = "http://www.w3.org/1999/xhtml";
+ return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
+ },
+
+ isSVGNode() {
+ const svgNs = "http://www.w3.org/2000/svg";
+ return this.isNode() && this.nodeFront.namespaceURI == svgNs;
+ },
+
+ isMathMLNode() {
+ const mathmlNs = "http://www.w3.org/1998/Math/MathML";
+ return this.isNode() && this.nodeFront.namespaceURI == mathmlNs;
+ },
+
+ // Node type
+
+ isElementNode() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE
+ );
+ },
+
+ isPseudoElementNode() {
+ return this.isNode() && this.nodeFront.isPseudoElement;
+ },
+
+ isAnonymousNode() {
+ return this.isNode() && this.nodeFront.isAnonymous;
+ },
+
+ isAttributeNode() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE
+ );
+ },
+
+ isTextNode() {
+ return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE;
+ },
+
+ isCDATANode() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE
+ );
+ },
+
+ isEntityRefNode() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE
+ );
+ },
+
+ isEntityNode() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE
+ );
+ },
+
+ isProcessingInstructionNode() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
+ );
+ },
+
+ isCommentNode() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE
+ );
+ },
+
+ isDocumentNode() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE
+ );
+ },
+
+ /**
+ * @returns true if the selection is the <body> HTML element.
+ */
+ isBodyNode() {
+ return (
+ this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "BODY"
+ );
+ },
+
+ /**
+ * @returns true if the selection is the <head> HTML element.
+ */
+ isHeadNode() {
+ return (
+ this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "HEAD"
+ );
+ },
+
+ isDocumentTypeNode() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE
+ );
+ },
+
+ isDocumentFragmentNode() {
+ return (
+ this.isNode() &&
+ this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE
+ );
+ },
+
+ isNotationNode() {
+ return (
+ this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE
+ );
+ },
+
+ isSlotted() {
+ return this._isSlotted;
+ },
+
+ isShadowRootNode() {
+ return this.isNode() && this.nodeFront.isShadowRoot;
+ },
+};
+
+module.exports = Selection;