summaryrefslogtreecommitdiff
path: root/toolkit/devtools/framework/selection.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/devtools/framework/selection.js')
-rw-r--r--toolkit/devtools/framework/selection.js332
1 files changed, 332 insertions, 0 deletions
diff --git a/toolkit/devtools/framework/selection.js b/toolkit/devtools/framework/selection.js
new file mode 100644
index 000000000..fed838674
--- /dev/null
+++ b/toolkit/devtools/framework/selection.js
@@ -0,0 +1,332 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {Cu, Ci} = require("chrome");
+let EventEmitter = require("devtools/toolkit/event-emitter");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
+
+/**
+ * API
+ *
+ * new Selection(walker=null, node=null, track={attributes,detached});
+ * destroy()
+ * node (readonly)
+ * setNode(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" when the inner node changed
+ * "before-new-node" when the inner node is set to change
+ * "attribute-changed" when an attribute is changed (only if tracked)
+ * "detached" when the node (or one of its parents) is removed from the document (only if tracked)
+ * "reparented" when the node (or one of its parents) is moved under a different node (only if tracked)
+ */
+
+/**
+ * A Selection object. Hold a reference to a node.
+ * Includes some helpers, fire some helpful events.
+ *
+ * @param node Inner node.
+ * Can be null. Can be (un)set in the future via the "node" property;
+ * @param trackAttribute Tell if events should be fired when the attributes of
+ * the node change.
+ *
+ */
+function Selection(walker, node=null, track={attributes:true,detached:true}) {
+ EventEmitter.decorate(this);
+
+ this._onMutations = this._onMutations.bind(this);
+ this.track = track;
+ this.setWalker(walker);
+ this.setNode(node);
+}
+
+exports.Selection = Selection;
+
+Selection.prototype = {
+ _walker: null,
+ _node: null,
+
+ _onMutations: function(mutations) {
+ let attributeChange = false;
+ let pseudoChange = false;
+ let detached = false;
+ let parentNode = null;
+
+ for (let 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) {
+ let rawNode = null;
+ if (parentNode && parentNode.isLocal_toBeDeprecated()) {
+ rawNode = parentNode.rawNode();
+ }
+
+ this.emit("detached", rawNode, null);
+ this.emit("detached-front", parentNode);
+ }
+ },
+
+ destroy: function() {
+ this.setNode(null);
+ this.setWalker(null);
+ },
+
+ setWalker: function(walker) {
+ if (this._walker) {
+ this._walker.off("mutations", this._onMutations);
+ }
+ this._walker = walker;
+ if (this._walker) {
+ this._walker.on("mutations", this._onMutations);
+ }
+ },
+
+ // Not remote-safe
+ setNode: function(value, reason="unknown") {
+ if (value) {
+ value = this._walker.frontForRawNode(value);
+ }
+ this.setNodeFront(value, reason);
+ },
+
+ // Not remote-safe
+ get node() {
+ return this._node;
+ },
+
+ // Not remote-safe
+ get window() {
+ if (this.isNode()) {
+ return this.node.ownerDocument.defaultView;
+ }
+ return null;
+ },
+
+ // Not remote-safe
+ get document() {
+ if (this.isNode()) {
+ return this.node.ownerDocument;
+ }
+ return null;
+ },
+
+ setNodeFront: function(value, reason="unknown") {
+ this.reason = reason;
+
+ // We used to return here if the node had not changed but we now need to
+ // set the node even if it is already set otherwise it is not possible to
+ // e.g. highlight the same node twice.
+ let rawValue = null;
+ if (value && value.isLocal_toBeDeprecated()) {
+ rawValue = value.rawNode();
+ }
+ this.emit("before-new-node", rawValue, reason);
+ this.emit("before-new-node-front", value, reason);
+ let previousNode = this._node;
+ let previousFront = this._nodeFront;
+ this._node = rawValue;
+ this._nodeFront = value;
+ this.emit("new-node", previousNode, this.reason);
+ this.emit("new-node-front", value, this.reason);
+ },
+
+ get documentFront() {
+ return this._walker.document(this._nodeFront);
+ },
+
+ get nodeFront() {
+ return this._nodeFront;
+ },
+
+ isRoot: function() {
+ return this.isNode() &&
+ this.isConnected() &&
+ this._nodeFront.isDocumentElement;
+ },
+
+ isNode: function() {
+ if (!this._nodeFront) {
+ return false;
+ }
+
+ // As long as tools are still accessing node.rawNode(),
+ // this needs to stay here.
+ if (this._node && Cu.isDeadWrapper(this._node)) {
+ return false;
+ }
+
+ return true;
+ },
+
+ isLocal: function() {
+ return !!this._node;
+ },
+
+ isConnected: function() {
+ let node = this._nodeFront;
+ if (!node || !node.actorID) {
+ return false;
+ }
+
+ // As long as there are still tools going around
+ // accessing node.rawNode, this needs to stay.
+ let rawNode = null;
+ if (node.isLocal_toBeDeprecated()) {
+ rawNode = node.rawNode();
+ }
+ if (rawNode) {
+ try {
+ let doc = this.document;
+ if (doc && doc.defaultView) {
+ let docEl = doc.documentElement;
+ let bindingParent = LayoutHelpers.getRootBindingParent(rawNode);
+
+ if (docEl.contains(bindingParent)) {
+ return true;
+ }
+ }
+ } catch (e) {
+ // "can't access dead object" error
+ }
+ return false;
+ }
+
+ while(node) {
+ if (node === this._walker.rootNode) {
+ return true;
+ }
+ node = node.parentNode();
+ };
+ return false;
+ },
+
+ isHTMLNode: function() {
+ let xhtml_ns = "http://www.w3.org/1999/xhtml";
+ return this.isNode() && this.nodeFront.namespaceURI == xhtml_ns;
+ },
+
+ // Node type
+
+ isElementNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE;
+ },
+
+ isPseudoElementNode: function() {
+ return this.isNode() && this.nodeFront.isPseudoElement;
+ },
+
+ isAnonymousNode: function() {
+ return this.isNode() && this.nodeFront.isAnonymous;
+ },
+
+ isAttributeNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ATTRIBUTE_NODE;
+ },
+
+ isTextNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.TEXT_NODE;
+ },
+
+ isCDATANode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.CDATA_SECTION_NODE;
+ },
+
+ isEntityRefNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_REFERENCE_NODE;
+ },
+
+ isEntityNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_NODE;
+ },
+
+ isProcessingInstructionNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE;
+ },
+
+ isCommentNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE;
+ },
+
+ isDocumentNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE;
+ },
+
+ /**
+ * @returns true if the selection is the <body> HTML element.
+ */
+ isBodyNode: function() {
+ return this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "BODY";
+ },
+
+ /**
+ * @returns true if the selection is the <head> HTML element.
+ */
+ isHeadNode: function() {
+ return this.isHTMLNode() &&
+ this.isConnected() &&
+ this.nodeFront.nodeName === "HEAD";
+ },
+
+ isDocumentTypeNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE;
+ },
+
+ isDocumentFragmentNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE;
+ },
+
+ isNotationNode: function() {
+ return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.NOTATION_NODE;
+ },
+};