michael@0: /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cu, Ci} = require("chrome"); michael@0: let EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: michael@0: /** michael@0: * API michael@0: * michael@0: * new Selection(walker=null, node=null, track={attributes,detached}); michael@0: * destroy() michael@0: * node (readonly) michael@0: * setNode(node, origin="unknown") michael@0: * michael@0: * Helpers: michael@0: * michael@0: * window michael@0: * document michael@0: * isRoot() michael@0: * isNode() michael@0: * isHTMLNode() michael@0: * michael@0: * Check the nature of the node: michael@0: * michael@0: * isElementNode() michael@0: * isAttributeNode() michael@0: * isTextNode() michael@0: * isCDATANode() michael@0: * isEntityRefNode() michael@0: * isEntityNode() michael@0: * isProcessingInstructionNode() michael@0: * isCommentNode() michael@0: * isDocumentNode() michael@0: * isDocumentTypeNode() michael@0: * isDocumentFragmentNode() michael@0: * isNotationNode() michael@0: * michael@0: * Events: michael@0: * "new-node" when the inner node changed michael@0: * "before-new-node" when the inner node is set to change michael@0: * "attribute-changed" when an attribute is changed (only if tracked) michael@0: * "detached" when the node (or one of its parents) is removed from the document (only if tracked) michael@0: * "reparented" when the node (or one of its parents) is moved under a different node (only if tracked) michael@0: */ michael@0: michael@0: /** michael@0: * A Selection object. Hold a reference to a node. michael@0: * Includes some helpers, fire some helpful events. michael@0: * michael@0: * @param node Inner node. michael@0: * Can be null. Can be (un)set in the future via the "node" property; michael@0: * @param trackAttribute Tell if events should be fired when the attributes of michael@0: * the node change. michael@0: * michael@0: */ michael@0: function Selection(walker, node=null, track={attributes:true,detached:true}) { michael@0: EventEmitter.decorate(this); michael@0: michael@0: this._onMutations = this._onMutations.bind(this); michael@0: this.track = track; michael@0: this.setWalker(walker); michael@0: this.setNode(node); michael@0: } michael@0: michael@0: exports.Selection = Selection; michael@0: michael@0: Selection.prototype = { michael@0: _walker: null, michael@0: _node: null, michael@0: michael@0: _onMutations: function(mutations) { michael@0: let attributeChange = false; michael@0: let pseudoChange = false; michael@0: let detached = false; michael@0: let parentNode = null; michael@0: michael@0: for (let m of mutations) { michael@0: if (!attributeChange && m.type == "attributes") { michael@0: attributeChange = true; michael@0: } michael@0: if (m.type == "childList") { michael@0: if (!detached && !this.isConnected()) { michael@0: if (this.isNode()) { michael@0: parentNode = m.target; michael@0: } michael@0: detached = true; michael@0: } michael@0: } michael@0: if (m.type == "pseudoClassLock") { michael@0: pseudoChange = true; michael@0: } michael@0: } michael@0: michael@0: // Fire our events depending on what changed in the mutations array michael@0: if (attributeChange) { michael@0: this.emit("attribute-changed"); michael@0: } michael@0: if (pseudoChange) { michael@0: this.emit("pseudoclass"); michael@0: } michael@0: if (detached) { michael@0: let rawNode = null; michael@0: if (parentNode && parentNode.isLocal_toBeDeprecated()) { michael@0: rawNode = parentNode.rawNode(); michael@0: } michael@0: michael@0: this.emit("detached", rawNode, null); michael@0: this.emit("detached-front", parentNode); michael@0: } michael@0: }, michael@0: michael@0: destroy: function() { michael@0: this.setNode(null); michael@0: this.setWalker(null); michael@0: }, michael@0: michael@0: setWalker: function(walker) { michael@0: if (this._walker) { michael@0: this._walker.off("mutations", this._onMutations); michael@0: } michael@0: this._walker = walker; michael@0: if (this._walker) { michael@0: this._walker.on("mutations", this._onMutations); michael@0: } michael@0: }, michael@0: michael@0: // Not remote-safe michael@0: setNode: function(value, reason="unknown") { michael@0: if (value) { michael@0: value = this._walker.frontForRawNode(value); michael@0: } michael@0: this.setNodeFront(value, reason); michael@0: }, michael@0: michael@0: // Not remote-safe michael@0: get node() { michael@0: return this._node; michael@0: }, michael@0: michael@0: // Not remote-safe michael@0: get window() { michael@0: if (this.isNode()) { michael@0: return this.node.ownerDocument.defaultView; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: // Not remote-safe michael@0: get document() { michael@0: if (this.isNode()) { michael@0: return this.node.ownerDocument; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: setNodeFront: function(value, reason="unknown") { michael@0: this.reason = reason; michael@0: michael@0: // We used to return here if the node had not changed but we now need to michael@0: // set the node even if it is already set otherwise it is not possible to michael@0: // e.g. highlight the same node twice. michael@0: let rawValue = null; michael@0: if (value && value.isLocal_toBeDeprecated()) { michael@0: rawValue = value.rawNode(); michael@0: } michael@0: this.emit("before-new-node", rawValue, reason); michael@0: this.emit("before-new-node-front", value, reason); michael@0: let previousNode = this._node; michael@0: let previousFront = this._nodeFront; michael@0: this._node = rawValue; michael@0: this._nodeFront = value; michael@0: this.emit("new-node", previousNode, this.reason); michael@0: this.emit("new-node-front", value, this.reason); michael@0: }, michael@0: michael@0: get documentFront() { michael@0: return this._walker.document(this._nodeFront); michael@0: }, michael@0: michael@0: get nodeFront() { michael@0: return this._nodeFront; michael@0: }, michael@0: michael@0: isRoot: function() { michael@0: return this.isNode() && michael@0: this.isConnected() && michael@0: this._nodeFront.isDocumentElement; michael@0: }, michael@0: michael@0: isNode: function() { michael@0: if (!this._nodeFront) { michael@0: return false; michael@0: } michael@0: michael@0: // As long as tools are still accessing node.rawNode(), michael@0: // this needs to stay here. michael@0: if (this._node && Cu.isDeadWrapper(this._node)) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: isLocal: function() { michael@0: return !!this._node; michael@0: }, michael@0: michael@0: isConnected: function() { michael@0: let node = this._nodeFront; michael@0: if (!node || !node.actorID) { michael@0: return false; michael@0: } michael@0: michael@0: // As long as there are still tools going around michael@0: // accessing node.rawNode, this needs to stay. michael@0: let rawNode = null; michael@0: if (node.isLocal_toBeDeprecated()) { michael@0: rawNode = node.rawNode(); michael@0: } michael@0: if (rawNode) { michael@0: try { michael@0: let doc = this.document; michael@0: return (doc && doc.defaultView && doc.documentElement.contains(rawNode)); michael@0: } catch (e) { michael@0: // "can't access dead object" error michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: while(node) { michael@0: if (node === this._walker.rootNode) { michael@0: return true; michael@0: } michael@0: node = node.parentNode(); michael@0: }; michael@0: return false; michael@0: }, michael@0: michael@0: isHTMLNode: function() { michael@0: let xhtml_ns = "http://www.w3.org/1999/xhtml"; michael@0: return this.isNode() && this.node.namespaceURI == xhtml_ns; michael@0: }, michael@0: michael@0: // Node type michael@0: michael@0: isElementNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE; michael@0: }, michael@0: michael@0: isAttributeNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ATTRIBUTE_NODE; michael@0: }, michael@0: michael@0: isTextNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.TEXT_NODE; michael@0: }, michael@0: michael@0: isCDATANode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.CDATA_SECTION_NODE; michael@0: }, michael@0: michael@0: isEntityRefNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_REFERENCE_NODE; michael@0: }, michael@0: michael@0: isEntityNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_NODE; michael@0: }, michael@0: michael@0: isProcessingInstructionNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; michael@0: }, michael@0: michael@0: isCommentNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; michael@0: }, michael@0: michael@0: isDocumentNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE; michael@0: }, michael@0: michael@0: isDocumentTypeNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE; michael@0: }, michael@0: michael@0: isDocumentFragmentNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE; michael@0: }, michael@0: michael@0: isNotationNode: function() { michael@0: return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.NOTATION_NODE; michael@0: }, michael@0: };