/* 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 { Cc, Ci, Cu } = require("chrome"); const { getCurrentZoom, getRootBindingParent } = require("devtools/shared/layout/utils"); const { on, emit } = require("sdk/event/core"); const lazyContainer = {}; loader.lazyRequireGetter(lazyContainer, "CssLogic", "devtools/server/css-logic", true); exports.getComputedStyle = (node) => lazyContainer.CssLogic.getComputedStyle(node); exports.getBindingElementAndPseudo = (node) => lazyContainer.CssLogic.getBindingElementAndPseudo(node); loader.lazyGetter(lazyContainer, "DOMUtils", () => Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)); exports.hasPseudoClassLock = (...args) => lazyContainer.DOMUtils.hasPseudoClassLock(...args); exports.addPseudoClassLock = (...args) => lazyContainer.DOMUtils.addPseudoClassLock(...args); exports.removePseudoClassLock = (...args) => lazyContainer.DOMUtils.removePseudoClassLock(...args); exports.getCSSStyleRules = (...args) => lazyContainer.DOMUtils.getCSSStyleRules(...args); const SVG_NS = "http://www.w3.org/2000/svg"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const STYLESHEET_URI = "resource://devtools/server/actors/" + "highlighters.css"; // How high is the infobar (px). const INFOBAR_HEIGHT = 34; // What's the size of the infobar arrow (px). const INFOBAR_ARROW_SIZE = 9; const _tokens = Symbol("classList/tokens"); /** * Shims the element's `classList` for anonymous content elements; used * internally by `CanvasFrameAnonymousContentHelper.getElement()` method. */ function ClassList(className) { let trimmed = (className || "").trim(); this[_tokens] = trimmed ? trimmed.split(/\s+/) : []; } ClassList.prototype = { item(index) { return this[_tokens][index]; }, contains(token) { return this[_tokens].includes(token); }, add(token) { if (!this.contains(token)) { this[_tokens].push(token); } emit(this, "update"); }, remove(token) { let index = this[_tokens].indexOf(token); if (index > -1) { this[_tokens].splice(index, 1); } emit(this, "update"); }, toggle(token) { if (this.contains(token)) { this.remove(token); } else { this.add(token); } }, get length() { return this[_tokens].length; }, [Symbol.iterator]: function* () { for (let i = 0; i < this.tokens.length; i++) { yield this[_tokens][i]; } }, toString() { return this[_tokens].join(" "); } }; /** * Is this content window a XUL window? * @param {Window} window * @return {Boolean} */ function isXUL(window) { return window.document.documentElement.namespaceURI === XUL_NS; } exports.isXUL = isXUL; /** * Inject a helper stylesheet in the window. */ var installedHelperSheets = new WeakMap(); function installHelperSheet(win, source, type = "agent") { if (installedHelperSheets.has(win.document)) { return; } let {Style} = require("sdk/stylesheet/style"); let {attach} = require("sdk/content/mod"); let style = Style({source, type}); attach(style, win); installedHelperSheets.set(win.document, style); } exports.installHelperSheet = installHelperSheet; /** * Returns true if a DOM node is "valid", where "valid" means that the node isn't a dead * object wrapper, is still attached to a document, and is of a given type. * @param {DOMNode} node * @param {Number} nodeType Optional, defaults to ELEMENT_NODE * @return {Boolean} */ function isNodeValid(node, nodeType = Ci.nsIDOMNode.ELEMENT_NODE) { // Is it still alive? if (!node || Cu.isDeadWrapper(node)) { return false; } // Is it of the right type? if (node.nodeType !== nodeType) { return false; } // Is its document accessible? let doc = node.ownerDocument; if (!doc || !doc.defaultView) { return false; } // Is the node connected to the document? Using getBindingParent adds // support for anonymous elements generated by a node in the document. let bindingParent = getRootBindingParent(node); if (!doc.documentElement.contains(bindingParent)) { return false; } return true; } exports.isNodeValid = isNodeValid; /** * Helper function that creates SVG DOM nodes. * @param {Window} This window's document will be used to create the element * @param {Object} Options for the node include: * - nodeType: the type of node, defaults to "box". * - attributes: a {name:value} object to be used as attributes for the node. * - prefix: a string that will be used to prefix the values of the id and class * attributes. * - parent: if provided, the newly created element will be appended to this * node. */ function createSVGNode(win, options) { if (!options.nodeType) { options.nodeType = "box"; } options.namespace = SVG_NS; return createNode(win, options); } exports.createSVGNode = createSVGNode; /** * Helper function that creates DOM nodes. * @param {Window} This window's document will be used to create the element * @param {Object} Options for the node include: * - nodeType: the type of node, defaults to "div". * - namespace: if passed, doc.createElementNS will be used instead of * doc.creatElement. * - attributes: a {name:value} object to be used as attributes for the node. * - prefix: a string that will be used to prefix the values of the id and class * attributes. * - parent: if provided, the newly created element will be appended to this * node. */ function createNode(win, options) { let type = options.nodeType || "div"; let node; if (options.namespace) { node = win.document.createElementNS(options.namespace, type); } else { node = win.document.createElement(type); } for (let name in options.attributes || {}) { let value = options.attributes[name]; if (options.prefix && (name === "class" || name === "id")) { value = options.prefix + value; } node.setAttribute(name, value); } if (options.parent) { options.parent.appendChild(node); } return node; } exports.createNode = createNode; /** * Every highlighters should insert their markup content into the document's * canvasFrame anonymous content container (see dom/webidl/Document.webidl). * * Since this container gets cleared when the document navigates, highlighters * should use this helper to have their markup content automatically re-inserted * in the new document. * * Since the markup content is inserted in the canvasFrame using * insertAnonymousContent, this means that it can be modified using the API * described in AnonymousContent.webidl. * To retrieve the AnonymousContent instance, use the content getter. * * @param {HighlighterEnv} highlighterEnv * The environemnt which windows will be used to insert the node. * @param {Function} nodeBuilder * A function that, when executed, returns a DOM node to be inserted into * the canvasFrame. */ function CanvasFrameAnonymousContentHelper(highlighterEnv, nodeBuilder) { this.highlighterEnv = highlighterEnv; this.nodeBuilder = nodeBuilder; this.anonymousContentDocument = this.highlighterEnv.document; // XXX the next line is a wallpaper for bug 1123362. this.anonymousContentGlobal = Cu.getGlobalForObject( this.anonymousContentDocument); // Only try to create the highlighter when the document is loaded, // otherwise, wait for the navigate event to fire. let doc = this.highlighterEnv.document; if (doc.documentElement && doc.readyState != "uninitialized") { this._insert(); } this._onNavigate = this._onNavigate.bind(this); this.highlighterEnv.on("navigate", this._onNavigate); this.listeners = new Map(); } CanvasFrameAnonymousContentHelper.prototype = { destroy: function () { try { let doc = this.anonymousContentDocument; doc.removeAnonymousContent(this._content); } catch (e) { // If the current window isn't the one the content was inserted into, this // will fail, but that's fine. } this.highlighterEnv.off("navigate", this._onNavigate); this.highlighterEnv = this.nodeBuilder = this._content = null; this.anonymousContentDocument = null; this.anonymousContentGlobal = null; this._removeAllListeners(); }, _insert: function () { let doc = this.highlighterEnv.document; // Insert the content node only if the document: // * is loaded (navigate event will fire once it is), // * still exists, // * isn't in XUL. if (doc.readyState == "uninitialized" || !doc.documentElement || isXUL(this.highlighterEnv.window)) { return; } // For now highlighters.css is injected in content as a ua sheet because //