diff --git a/.eslintignore b/.eslintignore index 5f57e358e20..fb4fdacb239 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,9 +4,6 @@ # Exclude expected objdirs. obj*/** -# Temporarily ignore HTML files that still need to be fixed. -devtools/**/*.html - # We ignore all these directories by default, until we get them enabled. # If you are enabling a directory, please add directory specific exclusions # below. @@ -17,7 +14,6 @@ caps/** chrome/** config/** db/** -devtools/** docshell/** dom/** editor/** @@ -91,24 +87,52 @@ browser/locales/** browser/extensions/loop/** # devtools/ exclusions -# Ignore d3 -devtools/client/shared/d3.js -devtools/client/webaudioeditor/lib/dagre-d3.js +devtools/*.js +devtools/client/*.js +devtools/client/aboutdebugging/** +devtools/client/animationinspector/** +devtools/client/canvasdebugger/** +devtools/client/commandline/** +devtools/client/debugger/** +devtools/client/eyedropper/** +devtools/client/framework/** +# devtools/client/inspector/shared/*.js files are eslint-clean, so they aren't +# included in the ignore list. +devtools/client/inspector/computed/** +devtools/client/inspector/fonts/** +devtools/client/inspector/layout/** +devtools/client/inspector/markup/** +devtools/client/inspector/rules/** +devtools/client/inspector/shared/test/** +devtools/client/inspector/test/** +devtools/client/inspector/*.js +devtools/client/jsonview/** +devtools/client/memory/** +devtools/client/netmonitor/** +devtools/client/performance/** +devtools/client/projecteditor/** +devtools/client/promisedebugger/** +devtools/client/responsivedesign/** +devtools/client/scratchpad/** +devtools/client/shadereditor/** +devtools/client/shared/** +devtools/client/sourceeditor/** +devtools/client/storage/** +devtools/client/styleeditor/** +devtools/client/tilt/** +devtools/client/webaudioeditor/** +devtools/client/webconsole/** +devtools/client/webide/** +devtools/server/** +devtools/shared/** -# Ignore codemirror -devtools/client/sourceeditor/codemirror/*.js -devtools/client/sourceeditor/codemirror/**/*.js -devtools/client/sourceeditor/test/codemirror/* - -# Ignore jquery test libs -devtools/client/markupview/test/lib_* - -# Ignore pre-processed files +# Ignore devtools pre-processed files devtools/client/framework/toolbox-process-window.js devtools/client/performance/system.js devtools/client/webide/webide-prefs.js +devtools/client/preferences/** -# Ignore various libs +# Ignore devtools third-party libs devtools/shared/jsbeautify/* devtools/shared/acorn/* devtools/client/sourceeditor/tern/* @@ -117,6 +141,12 @@ devtools/shared/sourcemap/* devtools/shared/qrcode/decoder/* devtools/shared/qrcode/encoder/* devtools/client/shared/vendor/* +devtools/client/shared/d3.js +devtools/client/webaudioeditor/lib/dagre-d3.js +devtools/client/sourceeditor/codemirror/*.js +devtools/client/sourceeditor/codemirror/**/*.js +devtools/client/sourceeditor/test/codemirror/* +devtools/client/markupview/test/lib_* # mobile/android/ exclusions mobile/android/chrome/content diff --git a/devtools/client/animationinspector/animation-controller.js b/devtools/client/animationinspector/animation-controller.js index def44654433..e5ad109223a 100644 --- a/devtools/client/animationinspector/animation-controller.js +++ b/devtools/client/animationinspector/animation-controller.js @@ -93,7 +93,9 @@ var getServerTraits = Task.async(function*(target) { { name: "hasSetCurrentTimes", actor: "animations", method: "setCurrentTimes" }, { name: "hasGetFrames", actor: "animationplayer", - method: "getFrames" } + method: "getFrames" }, + { name: "hasSetWalkerActor", actor: "animations", + method: "setWalkerActor" }, ]; let traits = {}; @@ -147,6 +149,12 @@ var AnimationsController = { return; } + // Let the AnimationsActor know what WalkerActor we're using. This will + // come in handy later to return references to DOM Nodes. + if (this.traits.hasSetWalkerActor) { + yield this.animationsFront.setWalkerActor(gInspector.walker); + } + this.startListeners(); yield this.onNewNodeFront(); diff --git a/devtools/client/animationinspector/components/animation-target-node.js b/devtools/client/animationinspector/components/animation-target-node.js index d903fb15e6c..b59b1323cc2 100644 --- a/devtools/client/animationinspector/components/animation-target-node.js +++ b/devtools/client/animationinspector/components/animation-target-node.js @@ -3,35 +3,22 @@ const {Cu} = require("chrome"); Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm"); const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); -const { - createNode, - TargetNodeHighlighter -} = require("devtools/client/animationinspector/utils"); +const {DomNodePreview} = require( + "devtools/client/inspector/shared/dom-node-preview"); -const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties"; -const L10N = new ViewHelpers.L10N(STRINGS_URI); +// Map dom node fronts by animation fronts so we don't have to get them from the +// walker every time the timeline is refreshed. +var nodeFronts = new WeakMap(); /** * UI component responsible for displaying a preview of the target dom node of * a given animation. - * @param {InspectorPanel} inspector Requires a reference to the inspector-panel - * to highlight and select the node, as well as refresh it when there are - * mutations. - * @param {Object} options Supported properties are: - * - compact {Boolean} Defaults to false. If true, nodes will be previewed like - * tag#id.class instead of + * Accepts the same parameters as the DomNodePreview component. See + * devtools/client/inspector/shared/dom-node-preview.js for documentation. */ -function AnimationTargetNode(inspector, options = {}) { +function AnimationTargetNode(inspector, options) { this.inspector = inspector; - this.options = options; - - this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this); - this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this); - this.onSelectNodeClick = this.onSelectNodeClick.bind(this); - this.onMarkupMutations = this.onMarkupMutations.bind(this); - this.onHighlightNodeClick = this.onHighlightNodeClick.bind(this); - this.onTargetHighlighterLocked = this.onTargetHighlighterLocked.bind(this); - + this.previewer = new DomNodePreview(inspector, options); EventEmitter.decorate(this); } @@ -39,282 +26,51 @@ exports.AnimationTargetNode = AnimationTargetNode; AnimationTargetNode.prototype = { init: function(containerEl) { - let document = containerEl.ownerDocument; - - // Init the markup for displaying the target node. - this.el = createNode({ - parent: containerEl, - attributes: { - "class": "animation-target" - } - }); - - // Icon to select the node in the inspector. - this.highlightNodeEl = createNode({ - parent: this.el, - nodeType: "span", - attributes: { - "class": "node-highlighter", - "title": L10N.getStr("node.highlightNodeLabel") - } - }); - - // Wrapper used for mouseover/out event handling. - this.previewEl = createNode({ - parent: this.el, - nodeType: "span", - attributes: { - "title": L10N.getStr("node.selectNodeLabel") - } - }); - - if (!this.options.compact) { - this.previewEl.appendChild(document.createTextNode("<")); - } - - // Tag name. - this.tagNameEl = createNode({ - parent: this.previewEl, - nodeType: "span", - attributes: { - "class": "tag-name theme-fg-color3" - } - }); - - // Id attribute container. - this.idEl = createNode({ - parent: this.previewEl, - nodeType: "span" - }); - - if (!this.options.compact) { - createNode({ - parent: this.idEl, - nodeType: "span", - attributes: { - "class": "attribute-name theme-fg-color2" - }, - textContent: "id" - }); - this.idEl.appendChild(document.createTextNode("=\"")); - } else { - createNode({ - parent: this.idEl, - nodeType: "span", - attributes: { - "class": "theme-fg-color2" - }, - textContent: "#" - }); - } - - createNode({ - parent: this.idEl, - nodeType: "span", - attributes: { - "class": "attribute-value theme-fg-color6" - } - }); - - if (!this.options.compact) { - this.idEl.appendChild(document.createTextNode("\"")); - } - - // Class attribute container. - this.classEl = createNode({ - parent: this.previewEl, - nodeType: "span" - }); - - if (!this.options.compact) { - createNode({ - parent: this.classEl, - nodeType: "span", - attributes: { - "class": "attribute-name theme-fg-color2" - }, - textContent: "class" - }); - this.classEl.appendChild(document.createTextNode("=\"")); - } else { - createNode({ - parent: this.classEl, - nodeType: "span", - attributes: { - "class": "theme-fg-color6" - }, - textContent: "." - }); - } - - createNode({ - parent: this.classEl, - nodeType: "span", - attributes: { - "class": "attribute-value theme-fg-color6" - } - }); - - if (!this.options.compact) { - this.classEl.appendChild(document.createTextNode("\"")); - this.previewEl.appendChild(document.createTextNode(">")); - } - - this.startListeners(); - }, - - startListeners: function() { - // Init events for highlighting and selecting the node. - this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); - this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut); - this.previewEl.addEventListener("click", this.onSelectNodeClick); - this.highlightNodeEl.addEventListener("click", this.onHighlightNodeClick); - - // Start to listen for markupmutation events. - this.inspector.on("markupmutation", this.onMarkupMutations); - - // Listen to the target node highlighter. - TargetNodeHighlighter.on("highlighted", this.onTargetHighlighterLocked); - }, - - stopListeners: function() { - TargetNodeHighlighter.off("highlighted", this.onTargetHighlighterLocked); - this.inspector.off("markupmutation", this.onMarkupMutations); - this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver); - this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut); - this.previewEl.removeEventListener("click", this.onSelectNodeClick); - this.highlightNodeEl.removeEventListener("click", this.onHighlightNodeClick); + this.previewer.init(containerEl); + this.isDestroyed = false; }, destroy: function() { - TargetNodeHighlighter.unhighlight().catch(e => console.error(e)); - - this.stopListeners(); - - this.el.remove(); - this.el = this.tagNameEl = this.idEl = this.classEl = null; - this.highlightNodeEl = this.previewEl = null; - this.nodeFront = this.inspector = this.playerFront = null; - }, - - get highlighterUtils() { - if (this.inspector && this.inspector.toolbox) { - return this.inspector.toolbox.highlighterUtils; - } - return null; - }, - - onPreviewMouseOver: function() { - if (!this.nodeFront || !this.highlighterUtils) { - return; - } - this.highlighterUtils.highlightNodeFront(this.nodeFront) - .catch(e => console.error(e)); - }, - - onPreviewMouseOut: function() { - if (!this.nodeFront || !this.highlighterUtils) { - return; - } - this.highlighterUtils.unhighlight() - .catch(e => console.error(e)); - }, - - onSelectNodeClick: function() { - if (!this.nodeFront) { - return; - } - this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector"); - }, - - onHighlightNodeClick: function(e) { - e.stopPropagation(); - - let classList = this.highlightNodeEl.classList; - - let isHighlighted = classList.contains("selected"); - if (isHighlighted) { - classList.remove("selected"); - TargetNodeHighlighter.unhighlight().then(() => { - this.emit("target-highlighter-unlocked"); - }, e => console.error(e)); - } else { - classList.add("selected"); - TargetNodeHighlighter.highlight(this).then(() => { - this.emit("target-highlighter-locked"); - }, e => console.error(e)); - } - }, - - onTargetHighlighterLocked: function(e, animationTargetNode) { - if (animationTargetNode !== this) { - this.highlightNodeEl.classList.remove("selected"); - } - }, - - onMarkupMutations: function(e, mutations) { - if (!this.nodeFront || !this.playerFront) { - return; - } - - for (let {target} of mutations) { - if (target === this.nodeFront) { - // Re-render with the same nodeFront to update the output. - this.render(this.playerFront); - break; - } - } + this.previewer.destroy(); + this.inspector = null; + this.isDestroyed = true; }, render: Task.async(function*(playerFront) { - this.playerFront = playerFront; - this.nodeFront = undefined; + // Get the nodeFront from the cache if it was stored previously. + let nodeFront = nodeFronts.get(playerFront); - try { - this.nodeFront = yield this.inspector.walker.getNodeFromActor( - playerFront.actorID, ["node"]); - } catch (e) { - if (!this.el) { - // The panel was destroyed in the meantime. Just log a warning. - console.warn("Cound't retrieve the animation target node, widget " + - "destroyed"); - } else { - // This was an unexpected error, log it. - console.error(e); - } - return; + // Try and get it from the playerFront directly next. + if (!nodeFront) { + nodeFront = playerFront.animationTargetNodeFront; } - if (!this.nodeFront || !this.el) { - return; - } - - let {tagName, attributes} = this.nodeFront; - - this.tagNameEl.textContent = tagName.toLowerCase(); - - let idIndex = attributes.findIndex(({name}) => name === "id"); - if (idIndex > -1 && attributes[idIndex].value) { - this.idEl.querySelector(".attribute-value").textContent = - attributes[idIndex].value; - this.idEl.style.display = "inline"; - } else { - this.idEl.style.display = "none"; - } - - let classIndex = attributes.findIndex(({name}) => name === "class"); - if (classIndex > -1 && attributes[classIndex].value) { - let value = attributes[classIndex].value; - if (this.options.compact) { - value = value.split(" ").join("."); + // Finally, get it from the walkerActor if it wasn't found. + if (!nodeFront) { + try { + nodeFront = yield this.inspector.walker.getNodeFromActor( + playerFront.actorID, ["node"]); + } catch (e) { + // If an error occured while getting the nodeFront and if it can't be + // attributed to the panel having been destroyed in the meantime, this + // error needs to be logged and render needs to stop. + if (!this.isDestroyed) { + console.error(e); + } + return; } - this.classEl.querySelector(".attribute-value").textContent = value; - this.classEl.style.display = "inline"; - } else { - this.classEl.style.display = "none"; + // In all cases, if by now the panel doesn't exist anymore, we need to + // stop rendering too. + if (this.isDestroyed) { + return; + } } + // Add the nodeFront to the cache. + nodeFronts.set(playerFront, nodeFront); + + this.previewer.render(nodeFront); this.emit("target-retrieved"); }) }; diff --git a/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js index 2d062c27b76..029ab4610e5 100644 --- a/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js +++ b/devtools/client/animationinspector/test/browser_animation_mutations_with_same_names.js @@ -25,7 +25,7 @@ add_task(function*() { // Reduce the known nodeFronts to a set to make them unique. let nodeFronts = new Set(panel.animationsTimelineComponent - .targetNodes.map(n => n.nodeFront)); + .targetNodes.map(n => n.previewer.nodeFront)); is(nodeFronts.size, 3, "The animations are applied to 3 different node fronts"); }); diff --git a/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js index b4b102e9463..b20291f9de9 100644 --- a/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js +++ b/devtools/client/animationinspector/test/browser_animation_playerWidgets_target_nodes.js @@ -16,16 +16,18 @@ add_task(function*() { yield selectNode(".animated", inspector); let targetNodeComponent = panel.animationsTimelineComponent.targetNodes[0]; + let {previewer} = targetNodeComponent; + // Make sure to wait for the target-retrieved event if the nodeFront hasn't // yet been retrieved by the TargetNodeComponent. - if (!targetNodeComponent.nodeFront) { + if (!previewer.nodeFront) { yield targetNodeComponent.once("target-retrieved"); } - is(targetNodeComponent.el.textContent, "div#.ball.animated", + is(previewer.el.textContent, "div#.ball.animated", "The target element's content is correct"); - let highlighterEl = targetNodeComponent.el.querySelector(".node-highlighter"); + let highlighterEl = previewer.el.querySelector(".node-highlighter"); ok(highlighterEl, "The icon to highlight the target element in the page exists"); }); diff --git a/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js index cd03e683223..58fa89d787b 100644 --- a/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js +++ b/devtools/client/animationinspector/test/browser_animation_target_highlight_select.js @@ -24,7 +24,7 @@ add_task(function*() { let targetNodeComponent = targets[0]; info("Retrieve the part of the widget that highlights the node on hover"); - let highlightingEl = targetNodeComponent.previewEl; + let highlightingEl = targetNodeComponent.previewer.previewEl; info("Listen to node-highlight event and mouse over the widget"); let onHighlight = toolbox.once("node-highlight"); @@ -39,7 +39,7 @@ add_task(function*() { highlightingEl.ownerDocument.defaultView); ok(true, "The node-highlight event was fired"); - is(targetNodeComponent.nodeFront, nodeFront, + is(targetNodeComponent.previewer.nodeFront, nodeFront, "The highlighted node is the one stored on the animation widget"); is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName"); @@ -60,12 +60,12 @@ add_task(function*() { "selection to change"); let onSelection = inspector.selection.once("new-node-front"); onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT); - let nodeEl = targetNodeComponent.previewEl; + let nodeEl = targetNodeComponent.previewer.previewEl; EventUtils.sendMouseEvent({type: "click"}, nodeEl, nodeEl.ownerDocument.defaultView); yield onSelection; - is(inspector.selection.nodeFront, targetNodeComponent.nodeFront, + is(inspector.selection.nodeFront, targetNodeComponent.previewer.nodeFront, "The selected node is the one stored on the animation widget"); yield onPanelUpdated; diff --git a/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js index 9c82797b2bb..ab83d7034be 100644 --- a/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js +++ b/devtools/client/animationinspector/test/browser_animation_target_highlighter_lock.js @@ -11,42 +11,44 @@ requestLongerTimeout(2); add_task(function*() { yield addTab(TEST_URL_ROOT + "doc_simple_animation.html"); - let {toolbox, inspector, panel} = yield openAnimationInspector(); + let {panel} = yield openAnimationInspector(); let targets = panel.animationsTimelineComponent.targetNodes; info("Click on the highlighter icon for the first animated node"); - yield lockHighlighterOn(targets[0]); - ok(targets[0].highlightNodeEl.classList.contains("selected"), + let domNodePreview1 = targets[0].previewer; + yield lockHighlighterOn(domNodePreview1); + ok(domNodePreview1.highlightNodeEl.classList.contains("selected"), "The highlighter icon is selected"); info("Click on the highlighter icon for the second animated node"); - yield lockHighlighterOn(targets[1]); - ok(targets[1].highlightNodeEl.classList.contains("selected"), + let domNodePreview2 = targets[1].previewer; + yield lockHighlighterOn(domNodePreview2); + ok(domNodePreview2.highlightNodeEl.classList.contains("selected"), "The highlighter icon is selected"); - ok(!targets[0].highlightNodeEl.classList.contains("selected"), + ok(!domNodePreview1.highlightNodeEl.classList.contains("selected"), "The highlighter icon for the first node is unselected"); info("Click again to unhighlight"); - yield unlockHighlighterOn(targets[1]); - ok(!targets[1].highlightNodeEl.classList.contains("selected"), + yield unlockHighlighterOn(domNodePreview2); + ok(!domNodePreview2.highlightNodeEl.classList.contains("selected"), "The highlighter icon for the second node is unselected"); }); -function* lockHighlighterOn(targetComponent) { - let onLocked = targetComponent.once("target-highlighter-locked"); - clickOnHighlighterIcon(targetComponent); +function* lockHighlighterOn(domNodePreview) { + let onLocked = domNodePreview.once("target-highlighter-locked"); + clickOnHighlighterIcon(domNodePreview); yield onLocked; } -function* unlockHighlighterOn(targetComponent) { - let onUnlocked = targetComponent.once("target-highlighter-unlocked"); - clickOnHighlighterIcon(targetComponent); +function* unlockHighlighterOn(domNodePreview) { + let onUnlocked = domNodePreview.once("target-highlighter-unlocked"); + clickOnHighlighterIcon(domNodePreview); yield onUnlocked; } -function clickOnHighlighterIcon(targetComponent) { - let lockEl = targetComponent.highlightNodeEl; +function clickOnHighlighterIcon(domNodePreview) { + let lockEl = domNodePreview.highlightNodeEl; EventUtils.sendMouseEvent({type: "click"}, lockEl, lockEl.ownerDocument.defaultView); } diff --git a/devtools/client/animationinspector/test/head.js b/devtools/client/animationinspector/test/head.js index 4743eb086dc..2db590865c8 100644 --- a/devtools/client/animationinspector/test/head.js +++ b/devtools/client/animationinspector/test/head.js @@ -351,7 +351,7 @@ function isNodeVisible(node) { var waitForAllAnimationTargets = Task.async(function*(panel) { let targets = panel.animationsTimelineComponent.targetNodes; yield promise.all(targets.map(t => { - if (!t.nodeFront) { + if (!t.previewer.nodeFront) { return t.once("target-retrieved"); } return false; diff --git a/devtools/client/animationinspector/utils.js b/devtools/client/animationinspector/utils.js index 984e6f6b826..12c4d486a63 100644 --- a/devtools/client/animationinspector/utils.js +++ b/devtools/client/animationinspector/utils.js @@ -146,43 +146,6 @@ function findOptimalTimeInterval(timeScale, exports.findOptimalTimeInterval = findOptimalTimeInterval; -/** - * The TargetNodeHighlighter util is a helper for AnimationTargetNode components - * that is used to lock the highlighter on animated nodes in the page. - * It instantiates a new highlighter that is then shared amongst all instances - * of AnimationTargetNode. This is useful because that means showing the - * highlighter on one animated node will unhighlight the previously highlighted - * one, but will not interfere with the default inspector highlighter. - */ -var TargetNodeHighlighter = { - highlighter: null, - isShown: false, - - highlight: Task.async(function*(animationTargetNode) { - if (!this.highlighter) { - let hUtils = animationTargetNode.inspector.toolbox.highlighterUtils; - this.highlighter = yield hUtils.getHighlighterByType("BoxModelHighlighter"); - } - - yield this.highlighter.show(animationTargetNode.nodeFront); - this.isShown = true; - this.emit("highlighted", animationTargetNode); - }), - - unhighlight: Task.async(function*() { - if (!this.highlighter || !this.isShown) { - return; - } - - yield this.highlighter.hide(); - this.isShown = false; - this.emit("unhighlighted"); - }) -}; - -EventEmitter.decorate(TargetNodeHighlighter); -exports.TargetNodeHighlighter = TargetNodeHighlighter; - /** * Format a timestamp (in ms) as a mm:ss.mmm string. * @param {Number} time diff --git a/devtools/client/inspector/shared/dom-node-preview.js b/devtools/client/inspector/shared/dom-node-preview.js new file mode 100644 index 00000000000..edac0de3b10 --- /dev/null +++ b/devtools/client/inspector/shared/dom-node-preview.js @@ -0,0 +1,333 @@ +/* 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} = require("chrome"); +Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm"); +const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); +const {createNode} = require("devtools/client/animationinspector/utils"); + +const STRINGS_URI = "chrome://devtools/locale/inspector.properties"; +const L10N = new ViewHelpers.L10N(STRINGS_URI); + +/** + * UI component responsible for displaying a preview of a dom node. + * @param {InspectorPanel} inspector Requires a reference to the inspector-panel + * to highlight and select the node, as well as refresh it when there are + * mutations. + * @param {Object} options Supported properties are: + * - compact {Boolean} Defaults to false. + * By default, nodes are previewed like + * If true, nodes will be previewed like tag#id.class instead. + */ +function DomNodePreview(inspector, options = {}) { + this.inspector = inspector; + this.options = options; + + this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this); + this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this); + this.onSelectElClick = this.onSelectElClick.bind(this); + this.onMarkupMutations = this.onMarkupMutations.bind(this); + this.onHighlightElClick = this.onHighlightElClick.bind(this); + this.onHighlighterLocked = this.onHighlighterLocked.bind(this); + + EventEmitter.decorate(this); +} + +exports.DomNodePreview = DomNodePreview; + +DomNodePreview.prototype = { + init: function(containerEl) { + let document = containerEl.ownerDocument; + + // Init the markup for displaying the target node. + this.el = createNode({ + parent: containerEl, + attributes: { + "class": "animation-target" + } + }); + + // Icon to select the node in the inspector. + this.highlightNodeEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "class": "node-highlighter", + "title": L10N.getStr("inspector.nodePreview.highlightNodeLabel") + } + }); + + // Wrapper used for mouseover/out event handling. + this.previewEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "title": L10N.getStr("inspector.nodePreview.selectNodeLabel") + } + }); + + if (!this.options.compact) { + this.previewEl.appendChild(document.createTextNode("<")); + } + + // Tag name. + this.tagNameEl = createNode({ + parent: this.previewEl, + nodeType: "span", + attributes: { + "class": "tag-name theme-fg-color3" + } + }); + + // Id attribute container. + this.idEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + if (!this.options.compact) { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "id" + }); + this.idEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "#" + }); + } + + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + if (!this.options.compact) { + this.idEl.appendChild(document.createTextNode("\"")); + } + + // Class attribute container. + this.classEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + if (!this.options.compact) { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "class" + }); + this.classEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "." + }); + } + + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + if (!this.options.compact) { + this.classEl.appendChild(document.createTextNode("\"")); + this.previewEl.appendChild(document.createTextNode(">")); + } + + this.startListeners(); + }, + + startListeners: function() { + // Init events for highlighting and selecting the node. + this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut); + this.previewEl.addEventListener("click", this.onSelectElClick); + this.highlightNodeEl.addEventListener("click", this.onHighlightElClick); + + // Start to listen for markupmutation events. + this.inspector.on("markupmutation", this.onMarkupMutations); + + // Listen to the target node highlighter. + HighlighterLock.on("highlighted", this.onHighlighterLocked); + }, + + stopListeners: function() { + HighlighterLock.off("highlighted", this.onHighlighterLocked); + this.inspector.off("markupmutation", this.onMarkupMutations); + this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut); + this.previewEl.removeEventListener("click", this.onSelectElClick); + this.highlightNodeEl.removeEventListener("click", this.onHighlightElClick); + }, + + destroy: function() { + HighlighterLock.unhighlight().catch(e => console.error(e)); + + this.stopListeners(); + + this.el.remove(); + this.el = this.tagNameEl = this.idEl = this.classEl = null; + this.highlightNodeEl = this.previewEl = null; + this.nodeFront = this.inspector = null; + }, + + get highlighterUtils() { + if (this.inspector && this.inspector.toolbox) { + return this.inspector.toolbox.highlighterUtils; + } + return null; + }, + + onPreviewMouseOver: function() { + if (!this.nodeFront || !this.highlighterUtils) { + return; + } + this.highlighterUtils.highlightNodeFront(this.nodeFront) + .catch(e => console.error(e)); + }, + + onPreviewMouseOut: function() { + if (!this.nodeFront || !this.highlighterUtils) { + return; + } + this.highlighterUtils.unhighlight() + .catch(e => console.error(e)); + }, + + onSelectElClick: function() { + if (!this.nodeFront) { + return; + } + this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview"); + }, + + onHighlightElClick: function(e) { + e.stopPropagation(); + + let classList = this.highlightNodeEl.classList; + let isHighlighted = classList.contains("selected"); + + if (isHighlighted) { + classList.remove("selected"); + HighlighterLock.unhighlight().then(() => { + this.emit("target-highlighter-unlocked"); + }, error => console.error(error)); + } else { + classList.add("selected"); + HighlighterLock.highlight(this).then(() => { + this.emit("target-highlighter-locked"); + }, error => console.error(error)); + } + }, + + onHighlighterLocked: function(e, domNodePreview) { + if (domNodePreview !== this) { + this.highlightNodeEl.classList.remove("selected"); + } + }, + + onMarkupMutations: function(e, mutations) { + if (!this.nodeFront) { + return; + } + + for (let {target} of mutations) { + if (target === this.nodeFront) { + // Re-render with the same nodeFront to update the output. + this.render(this.nodeFront); + break; + } + } + }, + + render: function(nodeFront) { + this.nodeFront = nodeFront; + let {tagName, attributes} = nodeFront; + + this.tagNameEl.textContent = tagName.toLowerCase(); + + let idIndex = attributes.findIndex(({name}) => name === "id"); + if (idIndex > -1 && attributes[idIndex].value) { + this.idEl.querySelector(".attribute-value").textContent = + attributes[idIndex].value; + this.idEl.style.display = "inline"; + } else { + this.idEl.style.display = "none"; + } + + let classIndex = attributes.findIndex(({name}) => name === "class"); + if (classIndex > -1 && attributes[classIndex].value) { + let value = attributes[classIndex].value; + if (this.options.compact) { + value = value.split(" ").join("."); + } + + this.classEl.querySelector(".attribute-value").textContent = value; + this.classEl.style.display = "inline"; + } else { + this.classEl.style.display = "none"; + } + } +}; + +/** + * HighlighterLock is a helper used to lock the highlighter on DOM nodes in the + * page. + * It instantiates a new highlighter that is then shared amongst all instances + * of DomNodePreview. This is useful because that means showing the highlighter + * on one node will unhighlight the previously highlighted one, but will not + * interfere with the default inspector highlighter. + */ +var HighlighterLock = { + highlighter: null, + isShown: false, + + highlight: Task.async(function*(animationTargetNode) { + if (!this.highlighter) { + let util = animationTargetNode.inspector.toolbox.highlighterUtils; + this.highlighter = yield util.getHighlighterByType("BoxModelHighlighter"); + } + + yield this.highlighter.show(animationTargetNode.nodeFront); + this.isShown = true; + this.emit("highlighted", animationTargetNode); + }), + + unhighlight: Task.async(function*() { + if (!this.highlighter || !this.isShown) { + return; + } + + yield this.highlighter.hide(); + this.isShown = false; + this.emit("unhighlighted"); + }) +}; + +EventEmitter.decorate(HighlighterLock); diff --git a/devtools/client/inspector/shared/moz.build b/devtools/client/inspector/shared/moz.build index fd20be71781..4767b5ed623 100644 --- a/devtools/client/inspector/shared/moz.build +++ b/devtools/client/inspector/shared/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. DevToolsModules( + 'dom-node-preview.js', 'style-inspector-menu.js', 'style-inspector-overlays.js', 'utils.js' diff --git a/devtools/client/inspector/shared/style-inspector-overlays.js b/devtools/client/inspector/shared/style-inspector-overlays.js index fd6a8310081..addf36c9fff 100644 --- a/devtools/client/inspector/shared/style-inspector-overlays.js +++ b/devtools/client/inspector/shared/style-inspector-overlays.js @@ -32,11 +32,16 @@ const TOOLTIP_IMAGE_TYPE = "image"; const TOOLTIP_FONTFAMILY_TYPE = "font-family"; // Types of nodes in the rule/computed-view -const VIEW_NODE_SELECTOR_TYPE = exports.VIEW_NODE_SELECTOR_TYPE = 1; -const VIEW_NODE_PROPERTY_TYPE = exports.VIEW_NODE_PROPERTY_TYPE = 2; -const VIEW_NODE_VALUE_TYPE = exports.VIEW_NODE_VALUE_TYPE = 3; -const VIEW_NODE_IMAGE_URL_TYPE = exports.VIEW_NODE_IMAGE_URL_TYPE = 4; -const VIEW_NODE_LOCATION_TYPE = exports.VIEW_NODE_LOCATION_TYPE = 5; +const VIEW_NODE_SELECTOR_TYPE = 1; +exports.VIEW_NODE_SELECTOR_TYPE = VIEW_NODE_SELECTOR_TYPE; +const VIEW_NODE_PROPERTY_TYPE = 2; +exports.VIEW_NODE_PROPERTY_TYPE = VIEW_NODE_PROPERTY_TYPE; +const VIEW_NODE_VALUE_TYPE = 3; +exports.VIEW_NODE_VALUE_TYPE = VIEW_NODE_VALUE_TYPE; +const VIEW_NODE_IMAGE_URL_TYPE = 4; +exports.VIEW_NODE_IMAGE_URL_TYPE = VIEW_NODE_IMAGE_URL_TYPE; +const VIEW_NODE_LOCATION_TYPE = 5; +exports.VIEW_NODE_LOCATION_TYPE = VIEW_NODE_LOCATION_TYPE; /** * Manages all highlighters in the style-inspector. @@ -183,9 +188,9 @@ HighlightersOverlay.prototype = { // promise. This causes some tests to fail when trying to install a // rejection handler on the result of the call. To avoid this, check // whether the result is truthy before installing the handler. - let promise = this.highlighters[this.highlighterShown].hide(); - if (promise) { - promise.then(null, e => console.error(e)); + let onHidden = this.highlighters[this.highlighterShown].hide(); + if (onHidden) { + onHidden.then(null, e => console.error(e)); } this.highlighterShown = null; diff --git a/devtools/client/inspector/shared/utils.js b/devtools/client/inspector/shared/utils.js index 815585455fd..e1fc3c00051 100644 --- a/devtools/client/inspector/shared/utils.js +++ b/devtools/client/inspector/shared/utils.js @@ -6,7 +6,7 @@ "use strict"; -const {Cc, Ci, Cu} = require("chrome"); +const {Ci, Cu} = require("chrome"); const {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {}); const {parseDeclarations} = @@ -28,7 +28,7 @@ const HTML_NS = "http://www.w3.org/1999/xhtml"; * @param {object} attributes * A set of attributes to set on the node. */ -function createChild(parent, tagName, attributes={}) { +function createChild(parent, tagName, attributes = {}) { let elt = parent.ownerDocument.createElementNS(HTML_NS, tagName); for (let attr in attributes) { if (attributes.hasOwnProperty(attr)) { diff --git a/devtools/client/locales/en-US/animationinspector.properties b/devtools/client/locales/en-US/animationinspector.properties index 1d17e67cdac..79575c91296 100644 --- a/devtools/client/locales/en-US/animationinspector.properties +++ b/devtools/client/locales/en-US/animationinspector.properties @@ -93,17 +93,3 @@ timeline.csstransition.nameLabel=%S - CSS Transition # This can happen if devtools couldn't figure out the type of the animation. # %S will be replaced by the name of the transition at run-time. timeline.unknown.nameLabel=%S - -# LOCALIZATION NOTE (node.selectNodeLabel): -# This string is displayed in a tooltip of the animation panel that is shown -# when hovering over an animated node (e.g. something like div.animated). -# The tooltip invites the user to click on the node in order to select it in the -# inspector panel. -node.selectNodeLabel=Click to select this node in the Inspector - -# LOCALIZATION NOTE (node.highlightNodeLabel): -# This string is displayed in a tooltip of the animation panel that is shown -# when hovering over the inspector icon displayed next to animated nodes. -# The tooltip invites the user to click on the icon in order to show the node -# highlighter. -node.highlightNodeLabel=Click to highlight this node in the page diff --git a/devtools/client/locales/en-US/inspector.properties b/devtools/client/locales/en-US/inspector.properties index 8851c5dd57b..1a837dbf250 100644 --- a/devtools/client/locales/en-US/inspector.properties +++ b/devtools/client/locales/en-US/inspector.properties @@ -116,3 +116,22 @@ inspector.menu.editAttribute.label=Edit Attribute %S # when the user right-clicks on the attribute of a node in the inspector, # and that allows to remove this attribute. inspector.menu.removeAttribute.label=Remove Attribute %S + +# LOCALIZATION NOTE (inspector.nodePreview.selectNodeLabel): +# This string is displayed in a tooltip that is shown when hovering over a DOM +# node preview (e.g. something like "div#foo.bar"). +# DOM node previews can be displayed in places like the animation-inspector, the +# console or the object inspector. +# The tooltip invites the user to click on the node in order to select it in the +# inspector panel. +inspector.nodePreview.selectNodeLabel=Click to select this node in the Inspector + +# LOCALIZATION NOTE (inspector.nodePreview.highlightNodeLabel): +# This string is displayed in a tooltip that is shown when hovering over a the +# inspector icon displayed next to a DOM node preview (e.g. next to something +# like "div#foo.bar"). +# DOM node previews can be displayed in places like the animation-inspector, the +# console or the object inspector. +# The tooltip invites the user to click on the icon in order to highlight the +# node in the page. +inspector.nodePreview.highlightNodeLabel=Click to highlight this node in the page diff --git a/devtools/server/actors/animation.js b/devtools/server/actors/animation.js index 6a8ec076216..3fe5ab4efa4 100644 --- a/devtools/server/actors/animation.js +++ b/devtools/server/actors/animation.js @@ -70,6 +70,7 @@ var AnimationPlayerActor = ActorClass({ this.onAnimationMutation = this.onAnimationMutation.bind(this); + this.walker = animationsActor.walker; this.tabActor = animationsActor.tabActor; this.player = player; this.node = player.effect.target; @@ -89,7 +90,9 @@ var AnimationPlayerActor = ActorClass({ if (this.observer && !Cu.isDeadWrapper(this.observer)) { this.observer.disconnect(); } - this.tabActor = this.player = this.node = this.styles = this.observer = null; + this.tabActor = this.player = this.node = this.styles = null; + this.observer = this.walker = null; + Actor.prototype.destroy.call(this); }, @@ -107,6 +110,12 @@ var AnimationPlayerActor = ActorClass({ let data = this.getCurrentState(); data.actor = this.actorID; + // If we know the WalkerActor, and if the animated node is known by it, then + // return its corresponding NodeActor ID too. + if (this.walker && this.walker.hasNode(this.node)) { + data.animationTargetNodeActorID = this.walker.getNode(this.node).actorID; + } + return data; }, @@ -381,6 +390,18 @@ var AnimationPlayerFront = FrontClass(AnimationPlayerActor, { Front.prototype.destroy.call(this); }, + /** + * If the AnimationsActor was given a reference to the WalkerActor previously + * then calling this getter will return the animation target NodeFront. + */ + get animationTargetNodeFront() { + if (!this._form.animationTargetNodeActorID) { + return null; + } + + return this.conn.getActor(this._form.animationTargetNodeActorID); + }, + /** * Getter for the initial state of the player. Up to date states can be * retrieved by calling the getCurrentState method. @@ -495,7 +516,7 @@ var AnimationsActor = exports.AnimationsActor = ActorClass({ events.off(this.tabActor, "navigate", this.onNavigate); this.stopAnimationPlayerUpdates(); - this.tabActor = this.observer = this.actors = null; + this.tabActor = this.observer = this.actors = this.walker = null; }, /** @@ -506,6 +527,23 @@ var AnimationsActor = exports.AnimationsActor = ActorClass({ this.destroy(); }, + /** + * Clients can optionally call this with a reference to their WalkerActor. + * If they do, then AnimationPlayerActor's forms are going to also include + * NodeActor IDs when the corresponding NodeActors do exist. + * This, in turns, is helpful for clients to avoid having to go back once more + * to the server to get a NodeActor for a particular animation. + * @param {WalkerActor} walker + */ + setWalkerActor: method(function(walker) { + this.walker = walker; + }, { + request: { + walker: Arg(0, "domwalker") + }, + response: {} + }), + /** * Retrieve the list of AnimationPlayerActor actors for currently running * animations on a node and its descendants. diff --git a/devtools/server/actors/inspector.js b/devtools/server/actors/inspector.js index 8a96c9ae20c..4d6ee532159 100644 --- a/devtools/server/actors/inspector.js +++ b/devtools/server/actors/inspector.js @@ -1367,7 +1367,7 @@ var WalkerActor = protocol.ActorClass({ let target = current.target; if (this._refMap.has(target)) { - let actor = this._refMap.get(target); + let actor = this.getNode(target); let mutation = { type: "events", target: actor.actorID, @@ -1469,13 +1469,30 @@ var WalkerActor = protocol.ActorClass({ protocol.Actor.prototype.unmanage.call(this, actor); }, - hasNode: function(node) { - return this._refMap.has(node); + /** + * Determine if the walker has come across this DOM node before. + * @param {DOMNode} rawNode + * @return {Boolean} + */ + hasNode: function(rawNode) { + return this._refMap.has(rawNode); + }, + + /** + * If the walker has come across this DOM node before, then get the + * corresponding node actor. + * @param {DOMNode} rawNode + * @return {NodeActor} + */ + getNode: function(rawNode) { + return this._refMap.get(rawNode); }, _ref: function(node) { - let actor = this._refMap.get(node); - if (actor) return actor; + let actor = this.getNode(node); + if (actor) { + return actor; + } actor = new NodeActor(this, node); @@ -1763,7 +1780,7 @@ var WalkerActor = protocol.ActorClass({ let child = walker.firstChild(); while (child) { - let childActor = this._refMap.get(child); + let childActor = this.getNode(child); if (childActor) { this.releaseNode(childActor, options); } @@ -1789,7 +1806,7 @@ var WalkerActor = protocol.ActorClass({ let walker = this.getDocumentWalker(node.rawNode); let cur; while ((cur = walker.parentNode())) { - let parent = this._refMap.get(cur); + let parent = this.getNode(cur); if (!parent) { // This parent didn't exist, so hasn't been seen by the client yet. newParents.add(this._ref(cur)); @@ -2942,7 +2959,7 @@ var WalkerActor = protocol.ActorClass({ events.emit(this, "any-mutation"); for (let change of mutations) { - let targetActor = this._refMap.get(change.target); + let targetActor = this.getNode(change.target); if (!targetActor) { continue; } @@ -2972,7 +2989,7 @@ var WalkerActor = protocol.ActorClass({ let removedActors = []; let addedActors = []; for (let removed of change.removedNodes) { - let removedActor = this._refMap.get(removed); + let removedActor = this.getNode(removed); if (!removedActor) { // If the client never encountered this actor we don't need to // mention that it was removed. @@ -2983,7 +3000,7 @@ var WalkerActor = protocol.ActorClass({ removedActors.push(removedActor.actorID); } for (let added of change.addedNodes) { - let addedActor = this._refMap.get(added); + let addedActor = this.getNode(added); if (!addedActor) { // If the client never encounted this actor we don't need to tell // it about its addition for ownership tree purposes - if the @@ -3021,7 +3038,7 @@ var WalkerActor = protocol.ActorClass({ return; } let frame = getFrameElement(window); - let frameActor = this._refMap.get(frame); + let frameActor = this.getNode(frame); if (!frameActor) { return; } @@ -3076,7 +3093,7 @@ var WalkerActor = protocol.ActorClass({ } let doc = window.document; - let documentActor = this._refMap.get(doc); + let documentActor = this.getNode(doc); if (!documentActor) { return; } @@ -3098,7 +3115,7 @@ var WalkerActor = protocol.ActorClass({ // they should reread the children list. this.queueMutation({ type: "childList", - target: this._refMap.get(parentNode).actorID, + target: this.getNode(parentNode).actorID, added: [], removed: [] });