1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/framework/selection.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,298 @@ 1.4 +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +const {Cu, Ci} = require("chrome"); 1.13 +let EventEmitter = require("devtools/toolkit/event-emitter"); 1.14 + 1.15 +/** 1.16 + * API 1.17 + * 1.18 + * new Selection(walker=null, node=null, track={attributes,detached}); 1.19 + * destroy() 1.20 + * node (readonly) 1.21 + * setNode(node, origin="unknown") 1.22 + * 1.23 + * Helpers: 1.24 + * 1.25 + * window 1.26 + * document 1.27 + * isRoot() 1.28 + * isNode() 1.29 + * isHTMLNode() 1.30 + * 1.31 + * Check the nature of the node: 1.32 + * 1.33 + * isElementNode() 1.34 + * isAttributeNode() 1.35 + * isTextNode() 1.36 + * isCDATANode() 1.37 + * isEntityRefNode() 1.38 + * isEntityNode() 1.39 + * isProcessingInstructionNode() 1.40 + * isCommentNode() 1.41 + * isDocumentNode() 1.42 + * isDocumentTypeNode() 1.43 + * isDocumentFragmentNode() 1.44 + * isNotationNode() 1.45 + * 1.46 + * Events: 1.47 + * "new-node" when the inner node changed 1.48 + * "before-new-node" when the inner node is set to change 1.49 + * "attribute-changed" when an attribute is changed (only if tracked) 1.50 + * "detached" when the node (or one of its parents) is removed from the document (only if tracked) 1.51 + * "reparented" when the node (or one of its parents) is moved under a different node (only if tracked) 1.52 + */ 1.53 + 1.54 +/** 1.55 + * A Selection object. Hold a reference to a node. 1.56 + * Includes some helpers, fire some helpful events. 1.57 + * 1.58 + * @param node Inner node. 1.59 + * Can be null. Can be (un)set in the future via the "node" property; 1.60 + * @param trackAttribute Tell if events should be fired when the attributes of 1.61 + * the node change. 1.62 + * 1.63 + */ 1.64 +function Selection(walker, node=null, track={attributes:true,detached:true}) { 1.65 + EventEmitter.decorate(this); 1.66 + 1.67 + this._onMutations = this._onMutations.bind(this); 1.68 + this.track = track; 1.69 + this.setWalker(walker); 1.70 + this.setNode(node); 1.71 +} 1.72 + 1.73 +exports.Selection = Selection; 1.74 + 1.75 +Selection.prototype = { 1.76 + _walker: null, 1.77 + _node: null, 1.78 + 1.79 + _onMutations: function(mutations) { 1.80 + let attributeChange = false; 1.81 + let pseudoChange = false; 1.82 + let detached = false; 1.83 + let parentNode = null; 1.84 + 1.85 + for (let m of mutations) { 1.86 + if (!attributeChange && m.type == "attributes") { 1.87 + attributeChange = true; 1.88 + } 1.89 + if (m.type == "childList") { 1.90 + if (!detached && !this.isConnected()) { 1.91 + if (this.isNode()) { 1.92 + parentNode = m.target; 1.93 + } 1.94 + detached = true; 1.95 + } 1.96 + } 1.97 + if (m.type == "pseudoClassLock") { 1.98 + pseudoChange = true; 1.99 + } 1.100 + } 1.101 + 1.102 + // Fire our events depending on what changed in the mutations array 1.103 + if (attributeChange) { 1.104 + this.emit("attribute-changed"); 1.105 + } 1.106 + if (pseudoChange) { 1.107 + this.emit("pseudoclass"); 1.108 + } 1.109 + if (detached) { 1.110 + let rawNode = null; 1.111 + if (parentNode && parentNode.isLocal_toBeDeprecated()) { 1.112 + rawNode = parentNode.rawNode(); 1.113 + } 1.114 + 1.115 + this.emit("detached", rawNode, null); 1.116 + this.emit("detached-front", parentNode); 1.117 + } 1.118 + }, 1.119 + 1.120 + destroy: function() { 1.121 + this.setNode(null); 1.122 + this.setWalker(null); 1.123 + }, 1.124 + 1.125 + setWalker: function(walker) { 1.126 + if (this._walker) { 1.127 + this._walker.off("mutations", this._onMutations); 1.128 + } 1.129 + this._walker = walker; 1.130 + if (this._walker) { 1.131 + this._walker.on("mutations", this._onMutations); 1.132 + } 1.133 + }, 1.134 + 1.135 + // Not remote-safe 1.136 + setNode: function(value, reason="unknown") { 1.137 + if (value) { 1.138 + value = this._walker.frontForRawNode(value); 1.139 + } 1.140 + this.setNodeFront(value, reason); 1.141 + }, 1.142 + 1.143 + // Not remote-safe 1.144 + get node() { 1.145 + return this._node; 1.146 + }, 1.147 + 1.148 + // Not remote-safe 1.149 + get window() { 1.150 + if (this.isNode()) { 1.151 + return this.node.ownerDocument.defaultView; 1.152 + } 1.153 + return null; 1.154 + }, 1.155 + 1.156 + // Not remote-safe 1.157 + get document() { 1.158 + if (this.isNode()) { 1.159 + return this.node.ownerDocument; 1.160 + } 1.161 + return null; 1.162 + }, 1.163 + 1.164 + setNodeFront: function(value, reason="unknown") { 1.165 + this.reason = reason; 1.166 + 1.167 + // We used to return here if the node had not changed but we now need to 1.168 + // set the node even if it is already set otherwise it is not possible to 1.169 + // e.g. highlight the same node twice. 1.170 + let rawValue = null; 1.171 + if (value && value.isLocal_toBeDeprecated()) { 1.172 + rawValue = value.rawNode(); 1.173 + } 1.174 + this.emit("before-new-node", rawValue, reason); 1.175 + this.emit("before-new-node-front", value, reason); 1.176 + let previousNode = this._node; 1.177 + let previousFront = this._nodeFront; 1.178 + this._node = rawValue; 1.179 + this._nodeFront = value; 1.180 + this.emit("new-node", previousNode, this.reason); 1.181 + this.emit("new-node-front", value, this.reason); 1.182 + }, 1.183 + 1.184 + get documentFront() { 1.185 + return this._walker.document(this._nodeFront); 1.186 + }, 1.187 + 1.188 + get nodeFront() { 1.189 + return this._nodeFront; 1.190 + }, 1.191 + 1.192 + isRoot: function() { 1.193 + return this.isNode() && 1.194 + this.isConnected() && 1.195 + this._nodeFront.isDocumentElement; 1.196 + }, 1.197 + 1.198 + isNode: function() { 1.199 + if (!this._nodeFront) { 1.200 + return false; 1.201 + } 1.202 + 1.203 + // As long as tools are still accessing node.rawNode(), 1.204 + // this needs to stay here. 1.205 + if (this._node && Cu.isDeadWrapper(this._node)) { 1.206 + return false; 1.207 + } 1.208 + 1.209 + return true; 1.210 + }, 1.211 + 1.212 + isLocal: function() { 1.213 + return !!this._node; 1.214 + }, 1.215 + 1.216 + isConnected: function() { 1.217 + let node = this._nodeFront; 1.218 + if (!node || !node.actorID) { 1.219 + return false; 1.220 + } 1.221 + 1.222 + // As long as there are still tools going around 1.223 + // accessing node.rawNode, this needs to stay. 1.224 + let rawNode = null; 1.225 + if (node.isLocal_toBeDeprecated()) { 1.226 + rawNode = node.rawNode(); 1.227 + } 1.228 + if (rawNode) { 1.229 + try { 1.230 + let doc = this.document; 1.231 + return (doc && doc.defaultView && doc.documentElement.contains(rawNode)); 1.232 + } catch (e) { 1.233 + // "can't access dead object" error 1.234 + return false; 1.235 + } 1.236 + } 1.237 + 1.238 + while(node) { 1.239 + if (node === this._walker.rootNode) { 1.240 + return true; 1.241 + } 1.242 + node = node.parentNode(); 1.243 + }; 1.244 + return false; 1.245 + }, 1.246 + 1.247 + isHTMLNode: function() { 1.248 + let xhtml_ns = "http://www.w3.org/1999/xhtml"; 1.249 + return this.isNode() && this.node.namespaceURI == xhtml_ns; 1.250 + }, 1.251 + 1.252 + // Node type 1.253 + 1.254 + isElementNode: function() { 1.255 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE; 1.256 + }, 1.257 + 1.258 + isAttributeNode: function() { 1.259 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ATTRIBUTE_NODE; 1.260 + }, 1.261 + 1.262 + isTextNode: function() { 1.263 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.TEXT_NODE; 1.264 + }, 1.265 + 1.266 + isCDATANode: function() { 1.267 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.CDATA_SECTION_NODE; 1.268 + }, 1.269 + 1.270 + isEntityRefNode: function() { 1.271 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_REFERENCE_NODE; 1.272 + }, 1.273 + 1.274 + isEntityNode: function() { 1.275 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ENTITY_NODE; 1.276 + }, 1.277 + 1.278 + isProcessingInstructionNode: function() { 1.279 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; 1.280 + }, 1.281 + 1.282 + isCommentNode: function() { 1.283 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; 1.284 + }, 1.285 + 1.286 + isDocumentNode: function() { 1.287 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE; 1.288 + }, 1.289 + 1.290 + isDocumentTypeNode: function() { 1.291 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE; 1.292 + }, 1.293 + 1.294 + isDocumentFragmentNode: function() { 1.295 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE; 1.296 + }, 1.297 + 1.298 + isNotationNode: function() { 1.299 + return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.NOTATION_NODE; 1.300 + }, 1.301 +};