mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1014547 - Add a css transform highlighter to the style-inspector; r=bgrins
This commit is contained in:
parent
8b0f429a75
commit
f7fa4585a3
@ -71,3 +71,10 @@ html|*.highlighter-nodeinfobar-tagname {
|
||||
html|*.highlighter-nodeinfobar-tagname {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
/*
|
||||
* Css transform highlighter
|
||||
*/
|
||||
svg|svg.css-transform-root[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
286
browser/devtools/framework/toolbox-highlighter-utils.js
Normal file
286
browser/devtools/framework/toolbox-highlighter-utils.js
Normal file
@ -0,0 +1,286 @@
|
||||
/* 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 {Promise: promise} = require("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource:///modules/devtools/gDevTools.jsm");
|
||||
|
||||
/**
|
||||
* Client-side highlighter shared module.
|
||||
* To be used by toolbox panels that need to highlight DOM elements.
|
||||
*
|
||||
* Highlighting and selecting elements is common enough that it needs to be at
|
||||
* toolbox level, accessible by any panel that needs it.
|
||||
* That's why the toolbox is the one that initializes the inspector and
|
||||
* highlighter. It's also why the API returned by this module needs a reference
|
||||
* to the toolbox which should be set once only.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the highighterUtils instance for a given toolbox.
|
||||
* This should be done once only by the toolbox itself and stored there so that
|
||||
* panels can get it from there. That's because the API returned has a stateful
|
||||
* scope that would be different for another instance returned by this function.
|
||||
*
|
||||
* @param {Toolbox} toolbox
|
||||
* @return {Object} the highlighterUtils public API
|
||||
*/
|
||||
exports.getHighlighterUtils = function(toolbox) {
|
||||
if (!toolbox || !toolbox.target) {
|
||||
throw new Error("Missing or invalid toolbox passed to getHighlighterUtils");
|
||||
return;
|
||||
}
|
||||
|
||||
// Exported API properties will go here
|
||||
let exported = {};
|
||||
|
||||
// The current toolbox target
|
||||
let target = toolbox.target;
|
||||
|
||||
// Is the highlighter currently in pick mode
|
||||
let isPicking = false;
|
||||
|
||||
/**
|
||||
* Release this utils, nullifying the references to the toolbox
|
||||
*/
|
||||
exported.release = function() {
|
||||
toolbox = target = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the target have the highlighter actor.
|
||||
* The devtools must be backwards compatible with at least B2G 1.3 (28),
|
||||
* which doesn't have the highlighter actor. This can be removed as soon as
|
||||
* the minimal supported version becomes 1.4 (29)
|
||||
*/
|
||||
let isRemoteHighlightable = exported.isRemoteHighlightable = function() {
|
||||
return target.client.traits.highlightable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the target support custom highlighters.
|
||||
*/
|
||||
let supportsCustomHighlighters = function() {
|
||||
return !!target.client.traits.customHighlighters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is typeName a known custom highlighter
|
||||
* @param {String} typeName
|
||||
* @return {Boolean}
|
||||
*/
|
||||
let hasCustomHighlighter = exported.hasCustomHighlighter = function(typeName) {
|
||||
return supportsCustomHighlighters() &&
|
||||
target.client.traits.customHighlighters.indexOf(typeName) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a function that initializes the inspector before it runs.
|
||||
* Since the init of the inspector is asynchronous, the return value will be
|
||||
* produced by Task.async and the argument should be a generator
|
||||
* @param {Function*} generator A generator function
|
||||
* @return {Function} A function
|
||||
*/
|
||||
let isInspectorInitialized = false;
|
||||
let requireInspector = generator => {
|
||||
return Task.async(function*(...args) {
|
||||
if (!isInspectorInitialized) {
|
||||
yield toolbox.initInspector();
|
||||
isInspectorInitialized = true;
|
||||
}
|
||||
return yield generator.apply(null, args);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Start/stop the element picker on the debuggee target.
|
||||
* @return A promise that resolves when done
|
||||
*/
|
||||
let togglePicker = exported.togglePicker = function() {
|
||||
if (isPicking) {
|
||||
return stopPicker();
|
||||
} else {
|
||||
return startPicker();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the element picker on the debuggee target.
|
||||
* This will request the inspector actor to start listening for mouse events
|
||||
* on the target page to highlight the hovered/picked element.
|
||||
* Depending on the server-side capabilities, this may fire events when nodes
|
||||
* are hovered.
|
||||
* @return A promise that resolves when the picker has started or immediately
|
||||
* if it is already started
|
||||
*/
|
||||
let startPicker = exported.startPicker = requireInspector(function*() {
|
||||
if (isPicking) {
|
||||
return;
|
||||
}
|
||||
isPicking = true;
|
||||
|
||||
toolbox.pickerButtonChecked = true;
|
||||
yield toolbox.selectTool("inspector");
|
||||
toolbox.on("select", stopPicker);
|
||||
|
||||
if (isRemoteHighlightable()) {
|
||||
toolbox.walker.on("picker-node-hovered", onPickerNodeHovered);
|
||||
toolbox.walker.on("picker-node-picked", onPickerNodePicked);
|
||||
|
||||
yield toolbox.highlighter.pick();
|
||||
toolbox.emit("picker-started");
|
||||
} else {
|
||||
// If the target doesn't have the highlighter actor, we can use the
|
||||
// walker's pick method instead, knowing that it only responds when a node
|
||||
// is picked (instead of emitting events)
|
||||
toolbox.emit("picker-started");
|
||||
let node = yield toolbox.walker.pick();
|
||||
onPickerNodePicked({node: node});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Stop the element picker. Note that the picker is automatically stopped when
|
||||
* an element is picked
|
||||
* @return A promise that resolves when the picker has stopped or immediately
|
||||
* if it is already stopped
|
||||
*/
|
||||
let stopPicker = exported.stopPicker = requireInspector(function*() {
|
||||
if (!isPicking) {
|
||||
return;
|
||||
}
|
||||
isPicking = false;
|
||||
|
||||
toolbox.pickerButtonChecked = false;
|
||||
|
||||
if (isRemoteHighlightable()) {
|
||||
yield toolbox.highlighter.cancelPick();
|
||||
toolbox.walker.off("picker-node-hovered", onPickerNodeHovered);
|
||||
toolbox.walker.off("picker-node-picked", onPickerNodePicked);
|
||||
} else {
|
||||
// If the target doesn't have the highlighter actor, use the walker's
|
||||
// cancelPick method instead
|
||||
yield toolbox.walker.cancelPick();
|
||||
}
|
||||
|
||||
toolbox.off("select", stopPicker);
|
||||
toolbox.emit("picker-stopped");
|
||||
});
|
||||
|
||||
/**
|
||||
* When a node is hovered by the mouse when the highlighter is in picker mode
|
||||
* @param {Object} data Information about the node being hovered
|
||||
*/
|
||||
function onPickerNodeHovered(data) {
|
||||
toolbox.emit("picker-node-hovered", data.node);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a node has been picked while the highlighter is in picker mode
|
||||
* @param {Object} data Information about the picked node
|
||||
*/
|
||||
function onPickerNodePicked(data) {
|
||||
toolbox.selection.setNodeFront(data.node, "picker-node-picked");
|
||||
stopPicker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the box model highlighter on a node in the content page.
|
||||
* The node needs to be a NodeFront, as defined by the inspector actor
|
||||
* @see toolkit/devtools/server/actors/inspector.js
|
||||
* @param {NodeFront} nodeFront The node to highlight
|
||||
* @param {Object} options
|
||||
* @return A promise that resolves when the node has been highlighted
|
||||
*/
|
||||
let highlightNodeFront = exported.highlightNodeFront = requireInspector(
|
||||
function*(nodeFront, options={}) {
|
||||
if (!nodeFront) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRemoteHighlightable()) {
|
||||
yield toolbox.highlighter.showBoxModel(nodeFront, options);
|
||||
} else {
|
||||
// If the target doesn't have the highlighter actor, revert to the
|
||||
// walker's highlight method, which draws a simple outline
|
||||
yield toolbox.walker.highlight(nodeFront);
|
||||
}
|
||||
|
||||
toolbox.emit("node-highlight", nodeFront);
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a convenience method in case you don't have a nodeFront but a
|
||||
* valueGrip. This is often the case with VariablesView properties.
|
||||
* This method will simply translate the grip into a nodeFront and call
|
||||
* highlightNodeFront, so it has the same signature.
|
||||
* @see highlightNodeFront
|
||||
*/
|
||||
let highlightDomValueGrip = exported.highlightDomValueGrip = requireInspector(
|
||||
function*(valueGrip, options={}) {
|
||||
let nodeFront = yield gripToNodeFront(valueGrip);
|
||||
if (nodeFront) {
|
||||
yield highlightNodeFront(nodeFront, options);
|
||||
} else {
|
||||
throw new Error("The ValueGrip passed could not be translated to a NodeFront");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Translate a debugger value grip into a node front usable by the inspector
|
||||
* @param {ValueGrip}
|
||||
* @return a promise that resolves to the node front when done
|
||||
*/
|
||||
let gripToNodeFront = exported.gripToNodeFront = requireInspector(
|
||||
function*(grip) {
|
||||
return yield toolbox.walker.getNodeActorFromObjectActor(grip.actor);
|
||||
});
|
||||
|
||||
/**
|
||||
* Hide the highlighter.
|
||||
* @param {Boolean} forceHide Only really matters in test mode (when
|
||||
* gDevTools.testing is true). In test mode, hovering over several nodes in
|
||||
* the markup view doesn't hide/show the highlighter to ease testing. The
|
||||
* highlighter stays visible at all times, except when the mouse leaves the
|
||||
* markup view, which is when this param is passed to true
|
||||
* @return a promise that resolves when the highlighter is hidden
|
||||
*/
|
||||
let unhighlight = exported.unhighlight = Task.async(
|
||||
function*(forceHide=false) {
|
||||
forceHide = forceHide || !gDevTools.testing;
|
||||
|
||||
// Note that if isRemoteHighlightable is true, there's no need to hide the
|
||||
// highlighter as the walker uses setTimeout to hide it after some time
|
||||
if (forceHide && toolbox.highlighter && isRemoteHighlightable()) {
|
||||
yield toolbox.highlighter.hideBoxModel();
|
||||
}
|
||||
|
||||
toolbox.emit("node-unhighlight");
|
||||
});
|
||||
|
||||
/**
|
||||
* If the main, box-model, highlighter isn't enough, or if multiple
|
||||
* highlighters are needed in parallel, this method can be used to return a
|
||||
* new instance of a highlighter actor, given a type.
|
||||
* The type of the highlighter passed must be known by the server.
|
||||
* The highlighter actor returned will have the show(nodeFront) and hide()
|
||||
* methods and needs to be released by the consumer when not needed anymore.
|
||||
* @return a promise that resolves to the highlighter
|
||||
*/
|
||||
let getHighlighterByType = exported.getHighlighterByType = requireInspector(
|
||||
function*(typeName) {
|
||||
if (hasCustomHighlighter(typeName)) {
|
||||
return yield toolbox.inspector.getHighlighterByType(typeName);
|
||||
} else {
|
||||
throw "The target doesn't support creating highlighters by types or " +
|
||||
typeName + " is unknown";
|
||||
}
|
||||
});
|
||||
|
||||
// Return the public API
|
||||
return exported;
|
||||
};
|
@ -13,6 +13,7 @@ let {Cc, Ci, Cu} = require("chrome");
|
||||
let {Promise: promise} = require("resource://gre/modules/Promise.jsm");
|
||||
let EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
let Telemetry = require("devtools/shared/telemetry");
|
||||
let {getHighlighterUtils} = require("devtools/framework/toolbox-highlighter-utils");
|
||||
let HUDService = require("devtools/webconsole/hudservice");
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
@ -68,7 +69,7 @@ function Toolbox(target, selectedTool, hostType, hostOptions) {
|
||||
this._refreshHostTitle = this._refreshHostTitle.bind(this);
|
||||
this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this)
|
||||
this.destroy = this.destroy.bind(this);
|
||||
this.highlighterUtils = new ToolboxHighlighterUtils(this);
|
||||
this.highlighterUtils = getHighlighterUtils(this);
|
||||
this._highlighterReady = this._highlighterReady.bind(this);
|
||||
this._highlighterHidden = this._highlighterHidden.bind(this);
|
||||
|
||||
@ -187,13 +188,11 @@ Toolbox.prototype = {
|
||||
/**
|
||||
* Get the toolbox highlighter front. Note that it may not always have been
|
||||
* initialized first. Use `initInspector()` if needed.
|
||||
* Consider using highlighterUtils instead, it exposes the highlighter API in
|
||||
* a useful way for the toolbox panels
|
||||
*/
|
||||
get highlighter() {
|
||||
if (this.highlighterUtils.isRemoteHighlightable) {
|
||||
return this._highlighter;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return this._highlighter;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -581,6 +580,18 @@ Toolbox.prototype = {
|
||||
this._pickerButton.addEventListener("command", this._togglePicker, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Setter for the checked state of the picker button in the toolbar
|
||||
* @param {Boolean} isChecked
|
||||
*/
|
||||
set pickerButtonChecked(isChecked) {
|
||||
if (isChecked) {
|
||||
this._pickerButton.setAttribute("checked", "true");
|
||||
} else {
|
||||
this._pickerButton.removeAttribute("checked");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return all toolbox buttons (command buttons, plus any others that were
|
||||
* added manually).
|
||||
@ -1142,12 +1153,11 @@ Toolbox.prototype = {
|
||||
this._walker = yield this._inspector.getWalker();
|
||||
this._selection = new Selection(this._walker);
|
||||
|
||||
if (this.highlighterUtils.isRemoteHighlightable) {
|
||||
let autohide = !gDevTools.testing;
|
||||
|
||||
if (this.highlighterUtils.isRemoteHighlightable()) {
|
||||
this.walker.on("highlighter-ready", this._highlighterReady);
|
||||
this.walker.on("highlighter-hide", this._highlighterHidden);
|
||||
|
||||
let autohide = !gDevTools.testing;
|
||||
this._highlighter = yield this._inspector.getHighlighter(autohide);
|
||||
}
|
||||
}.bind(this));
|
||||
@ -1277,6 +1287,7 @@ Toolbox.prototype = {
|
||||
}
|
||||
let target = this._target;
|
||||
this._target = null;
|
||||
this.highlighterUtils.release();
|
||||
target.off("close", this.destroy);
|
||||
return target.destroy();
|
||||
}).then(() => {
|
||||
@ -1294,202 +1305,5 @@ Toolbox.prototype = {
|
||||
|
||||
_highlighterHidden: function() {
|
||||
this.emit("highlighter-hide");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The ToolboxHighlighterUtils is what you should use for anything related to
|
||||
* node highlighting and picking.
|
||||
* It encapsulates the logic to connecting to the HighlighterActor.
|
||||
*/
|
||||
function ToolboxHighlighterUtils(toolbox) {
|
||||
this.toolbox = toolbox;
|
||||
this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this);
|
||||
this._onPickerNodePicked = this._onPickerNodePicked.bind(this);
|
||||
this.stopPicker = this.stopPicker.bind(this);
|
||||
}
|
||||
|
||||
ToolboxHighlighterUtils.prototype = {
|
||||
/**
|
||||
* Indicates whether the highlighter actor exists on the server.
|
||||
*/
|
||||
get isRemoteHighlightable() {
|
||||
return this.toolbox._target.client.traits.highlightable;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start/stop the element picker on the debuggee target.
|
||||
*/
|
||||
togglePicker: function() {
|
||||
if (this._isPicking) {
|
||||
return this.stopPicker();
|
||||
} else {
|
||||
return this.startPicker();
|
||||
}
|
||||
},
|
||||
|
||||
_onPickerNodeHovered: function(res) {
|
||||
this.toolbox.emit("picker-node-hovered", res.node);
|
||||
},
|
||||
|
||||
_onPickerNodePicked: function(res) {
|
||||
this.toolbox.selection.setNodeFront(res.node, "picker-node-picked");
|
||||
this.stopPicker();
|
||||
},
|
||||
|
||||
/**
|
||||
* Start the element picker on the debuggee target.
|
||||
* This will request the inspector actor to start listening for mouse/touch
|
||||
* events on the target to highlight the hovered/picked element.
|
||||
* Depending on the server-side capabilities, this may fire events when nodes
|
||||
* are hovered.
|
||||
* @return A promise that resolves when the picker has started or immediately
|
||||
* if it is already started
|
||||
*/
|
||||
startPicker: function() {
|
||||
if (this._isPicking) {
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
let deferred = promise.defer();
|
||||
|
||||
let done = () => {
|
||||
this._isPicking = true;
|
||||
this.toolbox.emit("picker-started");
|
||||
this.toolbox.on("select", this.stopPicker);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
promise.all([
|
||||
this.toolbox.initInspector(),
|
||||
this.toolbox.selectTool("inspector")
|
||||
]).then(() => {
|
||||
this.toolbox._pickerButton.setAttribute("checked", "true");
|
||||
|
||||
if (this.isRemoteHighlightable) {
|
||||
this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered);
|
||||
this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked);
|
||||
|
||||
this.toolbox.highlighter.pick().then(done);
|
||||
} else {
|
||||
return this.toolbox.walker.pick().then(node => {
|
||||
this.toolbox.selection.setNodeFront(node, "picker-node-picked").then(() => {
|
||||
this.stopPicker();
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop the element picker
|
||||
* @return A promise that resolves when the picker has stopped or immediately
|
||||
* if it is already stopped
|
||||
*/
|
||||
stopPicker: function() {
|
||||
if (!this._isPicking) {
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
let deferred = promise.defer();
|
||||
|
||||
let done = () => {
|
||||
this.toolbox.emit("picker-stopped");
|
||||
this.toolbox.off("select", this.stopPicker);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
this.toolbox.initInspector().then(() => {
|
||||
this._isPicking = false;
|
||||
this.toolbox._pickerButton.removeAttribute("checked");
|
||||
if (this.isRemoteHighlightable) {
|
||||
this.toolbox.highlighter.cancelPick().then(done);
|
||||
this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered);
|
||||
this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked);
|
||||
} else {
|
||||
this.toolbox.walker.cancelPick().then(done);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the box model highlighter on a node, given its NodeFront (this type
|
||||
* of front is normally returned by the WalkerActor).
|
||||
* @return a promise that resolves to the nodeFront when the node has been
|
||||
* highlit
|
||||
*/
|
||||
highlightNodeFront: function(nodeFront, options={}) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
// If the remote highlighter exists on the target, use it
|
||||
if (this.isRemoteHighlightable) {
|
||||
this.toolbox.initInspector().then(() => {
|
||||
this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => {
|
||||
this.toolbox.emit("node-highlight", nodeFront);
|
||||
deferred.resolve(nodeFront);
|
||||
});
|
||||
});
|
||||
}
|
||||
// Else, revert to the "older" version of the highlighter in the walker
|
||||
// actor
|
||||
else {
|
||||
this.toolbox.walker.highlight(nodeFront).then(() => {
|
||||
this.toolbox.emit("node-highlight", nodeFront);
|
||||
deferred.resolve(nodeFront);
|
||||
});
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* This is a convenience method in case you don't have a nodeFront but a
|
||||
* valueGrip. This is often the case with VariablesView properties.
|
||||
* This method will simply translate the grip into a nodeFront and call
|
||||
* highlightNodeFront
|
||||
* @return a promise that resolves to the nodeFront when the node has been
|
||||
* highlit
|
||||
*/
|
||||
highlightDomValueGrip: function(valueGrip, options={}) {
|
||||
return this._translateGripToNodeFront(valueGrip).then(nodeFront => {
|
||||
if (nodeFront) {
|
||||
return this.highlightNodeFront(nodeFront, options);
|
||||
} else {
|
||||
return promise.reject();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_translateGripToNodeFront: function(grip) {
|
||||
return this.toolbox.initInspector().then(() => {
|
||||
return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the highlighter.
|
||||
* @return a promise that resolves when the highlighter is hidden
|
||||
*/
|
||||
unhighlight: function(forceHide=false) {
|
||||
let unhighlightPromise;
|
||||
forceHide = forceHide || !gDevTools.testing;
|
||||
|
||||
if (forceHide && this.isRemoteHighlightable && this.toolbox.highlighter) {
|
||||
// If the remote highlighter exists on the target, use it
|
||||
unhighlightPromise = this.toolbox.highlighter.hideBoxModel();
|
||||
} else {
|
||||
// If not, no need to unhighlight as the older highlight method uses a
|
||||
// setTimeout to hide itself
|
||||
unhighlightPromise = promise.resolve();
|
||||
}
|
||||
|
||||
return unhighlightPromise.then(() => {
|
||||
this.toolbox.emit("node-unhighlight");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -42,7 +42,7 @@ function testNonTransformedBoxModelDimensionsNoZoom() {
|
||||
info("Highlighted the non-rotated div");
|
||||
isNodeCorrectlyHighlighted(div, "non-zoomed");
|
||||
|
||||
inspector.toolbox.once("highlighter-ready", testNonTransformedBoxModelDimensionsZoomed);
|
||||
waitForBoxModelUpdate().then(testNonTransformedBoxModelDimensionsZoomed);
|
||||
contentViewer = gBrowser.selectedBrowser.docShell.contentViewer
|
||||
.QueryInterface(Ci.nsIMarkupDocumentViewer);
|
||||
contentViewer.fullZoom = 2;
|
||||
@ -52,19 +52,22 @@ function testNonTransformedBoxModelDimensionsZoomed() {
|
||||
info("Highlighted the zoomed, non-rotated div");
|
||||
isNodeCorrectlyHighlighted(div, "zoomed");
|
||||
|
||||
inspector.toolbox.once("highlighter-ready", testMouseOverRotatedHighlights);
|
||||
waitForBoxModelUpdate().then(testMouseOverRotatedHighlights);
|
||||
contentViewer.fullZoom = 1;
|
||||
}
|
||||
|
||||
function testMouseOverRotatedHighlights() {
|
||||
inspector.toolbox.once("highlighter-ready", () => {
|
||||
ok(isHighlighting(), "Highlighter is shown");
|
||||
info("Highlighted the rotated div");
|
||||
isNodeCorrectlyHighlighted(rotated, "rotated");
|
||||
|
||||
executeSoon(finishUp);
|
||||
});
|
||||
let onBoxModelUpdate = waitForBoxModelUpdate();
|
||||
inspector.selection.setNode(rotated);
|
||||
inspector.once("inspector-updated", () => {
|
||||
onBoxModelUpdate.then(() => {
|
||||
ok(isHighlighting(), "Highlighter is shown");
|
||||
info("Highlighted the rotated div");
|
||||
isNodeCorrectlyHighlighted(rotated, "rotated");
|
||||
|
||||
executeSoon(finishUp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function finishUp() {
|
||||
|
@ -18,14 +18,18 @@ function test() {
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
info("Checking that the highlighter has the right size");
|
||||
let rect = getSimpleBorderRect();
|
||||
is(rect.width, 100, "outline has the right width");
|
||||
|
||||
waitForBoxModelUpdate().then(testRectWidth);
|
||||
|
||||
info("Changing the test element's size");
|
||||
div.style.width = "200px";
|
||||
inspector.toolbox.once("highlighter-ready", testRectWidth);
|
||||
}
|
||||
|
||||
function testRectWidth() {
|
||||
info("Checking that the highlighter has the right size after update");
|
||||
let rect = getSimpleBorderRect();
|
||||
is(rect.width, 200, "outline updated");
|
||||
finishUp();
|
||||
|
@ -309,6 +309,26 @@ function isHighlighting()
|
||||
return !root.hasAttribute("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes mutation changes on the box-model highlighter and returns a promise
|
||||
* that resolves when one of the attributes changes.
|
||||
* If an attribute changes in the box-model, it means its position/dimensions
|
||||
* got updated
|
||||
*/
|
||||
function waitForBoxModelUpdate() {
|
||||
let def = promise.defer();
|
||||
|
||||
let root = getBoxModelRoot();
|
||||
let polygon = root.querySelector(".box-model-content");
|
||||
let observer = new polygon.ownerDocument.defaultView.MutationObserver(() => {
|
||||
observer.disconnect();
|
||||
def.resolve();
|
||||
});
|
||||
observer.observe(polygon, {attributes: true});
|
||||
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
function getHighlitNode()
|
||||
{
|
||||
if (isHighlighting()) {
|
||||
@ -437,49 +457,15 @@ function isNodeCorrectlyHighlighted(node, prefix="") {
|
||||
prefix += (node.classList.length ? "." + [...node.classList].join(".") : "");
|
||||
prefix += " ";
|
||||
|
||||
let quads = helper.getAdjustedQuads(node, "content");
|
||||
let {p1:cp1, p2:cp2, p3:cp3, p4:cp4} = boxModel.content.points;
|
||||
is(cp1.x, quads.p1.x, prefix + "content point 1 x co-ordinate is correct");
|
||||
is(cp1.y, quads.p1.y, prefix + "content point 1 y co-ordinate is correct");
|
||||
is(cp2.x, quads.p2.x, prefix + "content point 2 x co-ordinate is correct");
|
||||
is(cp2.y, quads.p2.y, prefix + "content point 2 y co-ordinate is correct");
|
||||
is(cp3.x, quads.p3.x, prefix + "content point 3 x co-ordinate is correct");
|
||||
is(cp3.y, quads.p3.y, prefix + "content point 3 y co-ordinate is correct");
|
||||
is(cp4.x, quads.p4.x, prefix + "content point 4 x co-ordinate is correct");
|
||||
is(cp4.y, quads.p4.y, prefix + "content point 4 y co-ordinate is correct");
|
||||
|
||||
quads = helper.getAdjustedQuads(node, "padding");
|
||||
let {p1:pp1, p2:pp2, p3:pp3, p4:pp4} = boxModel.padding.points;
|
||||
is(pp1.x, quads.p1.x, prefix + "padding point 1 x co-ordinate is correct");
|
||||
is(pp1.y, quads.p1.y, prefix + "padding point 1 y co-ordinate is correct");
|
||||
is(pp2.x, quads.p2.x, prefix + "padding point 2 x co-ordinate is correct");
|
||||
is(pp2.y, quads.p2.y, prefix + "padding point 2 y co-ordinate is correct");
|
||||
is(pp3.x, quads.p3.x, prefix + "padding point 3 x co-ordinate is correct");
|
||||
is(pp3.y, quads.p3.y, prefix + "padding point 3 y co-ordinate is correct");
|
||||
is(pp4.x, quads.p4.x, prefix + "padding point 4 x co-ordinate is correct");
|
||||
is(pp4.y, quads.p4.y, prefix + "padding point 4 y co-ordinate is correct");
|
||||
|
||||
quads = helper.getAdjustedQuads(node, "border");
|
||||
let {p1:bp1, p2:bp2, p3:bp3, p4:bp4} = boxModel.border.points;
|
||||
is(bp1.x, quads.p1.x, prefix + "border point 1 x co-ordinate is correct");
|
||||
is(bp1.y, quads.p1.y, prefix + "border point 1 y co-ordinate is correct");
|
||||
is(bp2.x, quads.p2.x, prefix + "border point 2 x co-ordinate is correct");
|
||||
is(bp2.y, quads.p2.y, prefix + "border point 2 y co-ordinate is correct");
|
||||
is(bp3.x, quads.p3.x, prefix + "border point 3 x co-ordinate is correct");
|
||||
is(bp3.y, quads.p3.y, prefix + "border point 3 y co-ordinate is correct");
|
||||
is(bp4.x, quads.p4.x, prefix + "border point 4 x co-ordinate is correct");
|
||||
is(bp4.y, quads.p4.y, prefix + "border point 4 y co-ordinate is correct");
|
||||
|
||||
quads = helper.getAdjustedQuads(node, "margin");
|
||||
let {p1:mp1, p2:mp2, p3:mp3, p4:mp4} = boxModel.margin.points;
|
||||
is(mp1.x, quads.p1.x, prefix + "margin point 1 x co-ordinate is correct");
|
||||
is(mp1.y, quads.p1.y, prefix + "margin point 1 y co-ordinate is correct");
|
||||
is(mp2.x, quads.p2.x, prefix + "margin point 2 x co-ordinate is correct");
|
||||
is(mp2.y, quads.p2.y, prefix + "margin point 2 y co-ordinate is correct");
|
||||
is(mp3.x, quads.p3.x, prefix + "margin point 3 x co-ordinate is correct");
|
||||
is(mp3.y, quads.p3.y, prefix + "margin point 3 y co-ordinate is correct");
|
||||
is(mp4.x, quads.p4.x, prefix + "margin point 4 x co-ordinate is correct");
|
||||
is(mp4.y, quads.p4.y, prefix + "margin point 4 y co-ordinate is correct");
|
||||
for (let boxType of ["content", "padding", "border", "margin"]) {
|
||||
let quads = helper.getAdjustedQuads(node, boxType);
|
||||
for (let point in boxModel[boxType].points) {
|
||||
is(boxModel[boxType].points[point].x, quads[point].x,
|
||||
prefix + boxType + " point " + point + " x coordinate is correct");
|
||||
is(boxModel[boxType].points[point].y, quads[point].y,
|
||||
prefix + boxType + " point " + point + " y coordinate is correct");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getContainerForRawNode(markupView, rawNode)
|
||||
|
@ -24,8 +24,7 @@ let test = asyncTest(function*() {
|
||||
let {toolbox, inspector, view} = yield openLayoutView();
|
||||
|
||||
yield runTests(inspector, view);
|
||||
// TODO: Closing the toolbox in this test leaks - bug 994314
|
||||
// yield destroyToolbox(inspector);
|
||||
yield destroyToolbox(inspector);
|
||||
});
|
||||
|
||||
addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes",
|
||||
|
@ -23,8 +23,7 @@ let test = asyncTest(function*() {
|
||||
let {toolbox, inspector, view} = yield openLayoutView();
|
||||
|
||||
yield runTests(inspector, view);
|
||||
// TODO: Closing the toolbox in this test leaks - bug 994314
|
||||
// yield destroyToolbox(inspector);
|
||||
yield destroyToolbox(inspector);
|
||||
});
|
||||
|
||||
addTest("When all properties are set on the node editing one should work",
|
||||
|
@ -23,8 +23,7 @@ let test = asyncTest(function*() {
|
||||
let {toolbox, inspector, view} = yield openLayoutView();
|
||||
|
||||
yield runTests(inspector, view);
|
||||
// TODO: Closing the toolbox in this test leaks - bug 994314
|
||||
// yield destroyToolbox(inspector);
|
||||
yield destroyToolbox(inspector);
|
||||
});
|
||||
|
||||
addTest("Test that adding a border applies a border style when necessary",
|
||||
|
@ -24,8 +24,7 @@ let test = asyncTest(function*() {
|
||||
let {toolbox, inspector, view} = yield openLayoutView();
|
||||
|
||||
yield runTests(inspector, view);
|
||||
// TODO: Closing the toolbox in this test leaks - bug 994314
|
||||
// yield destroyToolbox(inspector);
|
||||
yield destroyToolbox(inspector);
|
||||
});
|
||||
|
||||
addTest("Test that entering units works",
|
||||
|
@ -175,10 +175,26 @@ MarkupView.prototype = {
|
||||
this._hoveredNode = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the box model highlighter on a given node front
|
||||
* @param {NodeFront} nodeFront The node to show the highlighter for
|
||||
* @param {Object} options Options for the highlighter
|
||||
* @return a promise that resolves when the highlighter for this nodeFront is
|
||||
* shown, taking into account that there could already be highlighter requests
|
||||
* queued up
|
||||
*/
|
||||
_showBoxModel: function(nodeFront, options={}) {
|
||||
this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
|
||||
return this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the box model highlighter on a given node front
|
||||
* @param {NodeFront} nodeFront The node to hide the highlighter for
|
||||
* @param {Boolean} forceHide See toolbox-highlighter-utils/unhighlight
|
||||
* @return a promise that resolves when the highlighter for this nodeFront is
|
||||
* hidden, taking into account that there could already be highlighter requests
|
||||
* queued up
|
||||
*/
|
||||
_hideBoxModel: function(forceHide) {
|
||||
return this._inspector.toolbox.highlighterUtils.unhighlight(forceHide);
|
||||
},
|
||||
|
@ -3,6 +3,7 @@ skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
browser_layoutHelpers.html
|
||||
browser_layoutHelpers-getBoxQuads.html
|
||||
browser_layoutHelpers_iframe.html
|
||||
browser_templater_basic.html
|
||||
browser_toolbar_basic.html
|
||||
@ -22,6 +23,7 @@ support-files =
|
||||
[browser_graphs-09.js]
|
||||
[browser_graphs-10.js]
|
||||
[browser_layoutHelpers.js]
|
||||
[browser_layoutHelpers-getBoxQuads.js]
|
||||
[browser_observableobject.js]
|
||||
[browser_outputparser.js]
|
||||
[browser_require_basic.js]
|
||||
@ -49,4 +51,3 @@ support-files =
|
||||
[browser_tableWidget_keyboard_interaction.js]
|
||||
[browser_tableWidget_mouse_interaction.js]
|
||||
[browser_spectrum.js]
|
||||
[browser_csstransformpreview.js]
|
||||
|
@ -1,139 +0,0 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that the spectrum color picker works correctly
|
||||
|
||||
const TEST_URI = "data:text/html;charset=utf-8,<div></div>";
|
||||
const {CSSTransformPreviewer} = devtools.require("devtools/shared/widgets/CSSTransformPreviewer");
|
||||
|
||||
let doc, root;
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
addTab(TEST_URI, () => {
|
||||
doc = content.document;
|
||||
root = doc.querySelector("div");
|
||||
startTests();
|
||||
});
|
||||
}
|
||||
|
||||
function endTests() {
|
||||
doc = root = null;
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
}
|
||||
|
||||
function startTests() {
|
||||
testCreateAndDestroyShouldAppendAndRemoveElements();
|
||||
}
|
||||
|
||||
function testCreateAndDestroyShouldAppendAndRemoveElements() {
|
||||
ok(root, "We have the root node to append the preview to");
|
||||
is(root.childElementCount, 0, "Root node is empty");
|
||||
|
||||
let p = new CSSTransformPreviewer(root);
|
||||
p.preview("matrix(1, -0.2, 0, 1, 0, 0)");
|
||||
ok(root.childElementCount > 0, "Preview has appended elements");
|
||||
ok(root.querySelector("canvas"), "Canvas preview element is here");
|
||||
|
||||
p.destroy();
|
||||
is(root.childElementCount, 0, "Destroying preview removed all nodes");
|
||||
|
||||
testCanvasDimensionIsConstrainedByMaxDim();
|
||||
}
|
||||
|
||||
function testCanvasDimensionIsConstrainedByMaxDim() {
|
||||
let p = new CSSTransformPreviewer(root);
|
||||
p.MAX_DIM = 500;
|
||||
p.preview("scale(1)", "center", 1000, 1000);
|
||||
|
||||
let canvas = root.querySelector("canvas");
|
||||
is(canvas.width, 500, "Canvas width is correct");
|
||||
is(canvas.height, 500, "Canvas height is correct");
|
||||
|
||||
p.destroy();
|
||||
|
||||
testCallingPreviewSeveralTimesReusesTheSameCanvas();
|
||||
}
|
||||
|
||||
function testCallingPreviewSeveralTimesReusesTheSameCanvas() {
|
||||
let p = new CSSTransformPreviewer(root);
|
||||
|
||||
p.preview("scale(1)", "center", 1000, 1000);
|
||||
let canvas = root.querySelector("canvas");
|
||||
|
||||
p.preview("rotate(90deg)");
|
||||
let canvases = root.querySelectorAll("canvas");
|
||||
is(canvases.length, 1, "Still one canvas element");
|
||||
is(canvases[0], canvas, "Still the same canvas element");
|
||||
p.destroy();
|
||||
|
||||
testCanvasDimensionAreCorrect();
|
||||
}
|
||||
|
||||
function testCanvasDimensionAreCorrect() {
|
||||
// Only test a few simple transformations
|
||||
let p = new CSSTransformPreviewer(root);
|
||||
|
||||
// Make sure we have a square
|
||||
let w = 200, h = w;
|
||||
p.MAX_DIM = w;
|
||||
|
||||
// We can't test the content of the canvas here, just that, given a max width
|
||||
// the aspect ratio of the canvas seems correct.
|
||||
|
||||
// Translate a square by its width, should be a rectangle
|
||||
p.preview("translateX(200px)", "center", w, h);
|
||||
let canvas = root.querySelector("canvas");
|
||||
is(canvas.width, w, "width is correct");
|
||||
is(canvas.height, h/2, "height is half of the width");
|
||||
|
||||
// Rotate on the top right corner, should be a rectangle
|
||||
p.preview("rotate(-90deg)", "top right", w, h);
|
||||
is(canvas.width, w, "width is correct");
|
||||
is(canvas.height, h/2, "height is half of the width");
|
||||
|
||||
// Rotate on the bottom left corner, should be a rectangle
|
||||
p.preview("rotate(90deg)", "top right", w, h);
|
||||
is(canvas.width, w/2, "width is half of the height");
|
||||
is(canvas.height, h, "height is correct");
|
||||
|
||||
// Scale from center, should still be a square
|
||||
p.preview("scale(2)", "center", w, h);
|
||||
is(canvas.width, w, "width is correct");
|
||||
is(canvas.height, h, "height is correct");
|
||||
|
||||
// Skew from center, 45deg, should be a rectangle
|
||||
p.preview("skew(45deg)", "center", w, h);
|
||||
is(canvas.width, w, "width is correct");
|
||||
is(canvas.height, h/2, "height is half of the height");
|
||||
|
||||
p.destroy();
|
||||
|
||||
testPreviewingInvalidTransformReturnsFalse();
|
||||
}
|
||||
|
||||
function testPreviewingInvalidTransformReturnsFalse() {
|
||||
let p = new CSSTransformPreviewer(root);
|
||||
ok(!p.preview("veryWow(muchPx) suchTransform(soDeg)"), "Returned false for invalid transform");
|
||||
ok(!p.preview("rotae(3deg)"), "Returned false for invalid transform");
|
||||
|
||||
// Verify the canvas is empty by checking the image data
|
||||
let canvas = root.querySelector("canvas"), ctx = canvas.getContext("2d");
|
||||
let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
for (let i = 0, n = data.length; i < n; i += 4) {
|
||||
// Let's not log 250*250*4 asserts! Instead, just log when it fails
|
||||
let red = data[i];
|
||||
let green = data[i + 1];
|
||||
let blue = data[i + 2];
|
||||
let alpha = data[i + 3];
|
||||
if (red !== 0 || green !== 0 || blue !== 0 || alpha !== 0) {
|
||||
ok(false, "Image data is not empty after an invalid transformed was previewed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
is(p.preview("translateX(30px)"), true, "Returned true for a valid transform");
|
||||
endTests();
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Layout Helpers</title>
|
||||
<style id="styles">
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#hidden-node {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#simple-node-with-margin-padding-border {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #f06;
|
||||
|
||||
padding: 20px;
|
||||
margin: 50px;
|
||||
border: 10px solid black;
|
||||
}
|
||||
|
||||
#scrolled-node {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
overflow: scroll;
|
||||
background: linear-gradient(red, pink);
|
||||
}
|
||||
|
||||
#sub-scrolled-node {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
overflow: scroll;
|
||||
background: linear-gradient(yellow, green);
|
||||
}
|
||||
|
||||
#inner-scrolled-node {
|
||||
width: 100px;
|
||||
height: 400px;
|
||||
background: linear-gradient(black, white);
|
||||
}
|
||||
</style>
|
||||
<div id="hidden-node"></div>
|
||||
<div id="simple-node-with-margin-padding-border"></div>
|
||||
<!-- The inline encoded code below corresponds to:
|
||||
<iframe style="margin:10px;border:0;width:300px;height:300px;">
|
||||
<iframe style="margin:10px;border:0;width:200px;height:200px;">
|
||||
<div id="inner-node" style="width:100px;height:100px;border:10px solid red;margin:10px;padding:10px;"></div>
|
||||
</iframe>
|
||||
</iframe>
|
||||
-->
|
||||
<iframe src="data:text/html,%3Cstyle%3Ebody%7Bmargin:0;padding:0;%7D%3C/style%3E%3Ciframe%20src=%22data:text/html,%253Cstyle%253Ebody%257Bmargin:0;padding:0;%257D%253C/style%253E%253Cdiv%2520id='inner-node'%2520style='width:100px;height:100px;border:10px%2520solid%2520red;margin:10px;padding:10px;'%253E%253C/div%253E%22%20style=%22margin:10px;border:0;width:200px;height:200px;%22%3E%3C/iframe%3E" style="margin:10px;border:0;width:300px;height:300px;"></iframe>
|
||||
<div id="scrolled-node">
|
||||
<div id="sub-scrolled-node">
|
||||
<div id="inner-scrolled-node"></div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,209 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that LayoutHelpers.getAdjustedQuads works properly in a variety of use
|
||||
// cases including iframes, scroll and zoom
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
const {LayoutHelpers} = Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", {});
|
||||
|
||||
const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers-getBoxQuads.html";
|
||||
|
||||
function test() {
|
||||
addTab(TEST_URI, function(browser, tab) {
|
||||
let doc = browser.contentDocument;
|
||||
let win = doc.defaultView;
|
||||
|
||||
info("Creating a new LayoutHelpers instance for the test window");
|
||||
let helper = new LayoutHelpers(win);
|
||||
ok(helper.getAdjustedQuads, "getAdjustedQuads is defined");
|
||||
|
||||
info("Running tests");
|
||||
|
||||
returnsTheRightDataStructure(doc, helper);
|
||||
returnsNullForMissingNode(doc, helper);
|
||||
returnsNullForHiddenNodes(doc, helper);
|
||||
defaultsToBorderBoxIfNoneProvided(doc, helper);
|
||||
returnsLikeGetBoxQuadsInSimpleCase(doc, helper);
|
||||
takesIframesOffsetsIntoAccount(doc, helper);
|
||||
takesScrollingIntoAccount(doc, helper);
|
||||
takesZoomIntoAccount(doc, helper);
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
function returnsTheRightDataStructure(doc, helper) {
|
||||
info("Checks that the returned data contains bounds and 4 points");
|
||||
|
||||
let node = doc.querySelector("body");
|
||||
let res = helper.getAdjustedQuads(node, "content");
|
||||
|
||||
ok("bounds" in res, "The returned data has a bounds property");
|
||||
ok("p1" in res, "The returned data has a p1 property");
|
||||
ok("p2" in res, "The returned data has a p2 property");
|
||||
ok("p3" in res, "The returned data has a p3 property");
|
||||
ok("p4" in res, "The returned data has a p4 property");
|
||||
|
||||
for (let boundProp of
|
||||
["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
|
||||
ok(boundProp in res.bounds, "The bounds has a " + boundProp + " property");
|
||||
}
|
||||
|
||||
for (let point of ["p1", "p2", "p3", "p4"]) {
|
||||
for (let pointProp of ["x", "y", "z", "w"]) {
|
||||
ok(pointProp in res[point], point + " has a " + pointProp + " property");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function returnsNullForMissingNode(doc, helper) {
|
||||
info("Checks that null is returned for invalid nodes");
|
||||
|
||||
for (let input of [null, undefined, "", 0]) {
|
||||
ok(helper.getAdjustedQuads(input) === null, "null is returned for input " +
|
||||
input);
|
||||
}
|
||||
}
|
||||
|
||||
function returnsNullForHiddenNodes(doc, helper) {
|
||||
info("Checks that null is returned for nodes that aren't rendered");
|
||||
|
||||
let style = doc.querySelector("#styles");
|
||||
ok(helper.getAdjustedQuads(style) === null,
|
||||
"null is returned for a <style> node");
|
||||
|
||||
let hidden = doc.querySelector("#hidden-node");
|
||||
ok(helper.getAdjustedQuads(hidden) === null,
|
||||
"null is returned for a hidden node");
|
||||
}
|
||||
|
||||
function defaultsToBorderBoxIfNoneProvided(doc, helper) {
|
||||
info("Checks that if no boxtype is passed, then border is the default one");
|
||||
|
||||
let node = doc.querySelector("#simple-node-with-margin-padding-border");
|
||||
let withBoxType = helper.getAdjustedQuads(node, "border");
|
||||
let withoutBoxType = helper.getAdjustedQuads(node);
|
||||
|
||||
for (let boundProp of
|
||||
["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
|
||||
is(withBoxType.bounds[boundProp], withoutBoxType.bounds[boundProp],
|
||||
boundProp + " bound is equal with or without the border box type");
|
||||
}
|
||||
|
||||
for (let point of ["p1", "p2", "p3", "p4"]) {
|
||||
for (let pointProp of ["x", "y", "z", "w"]) {
|
||||
is(withBoxType[point][pointProp], withoutBoxType[point][pointProp],
|
||||
point + "." + pointProp +
|
||||
" is equal with or without the border box type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function returnsLikeGetBoxQuadsInSimpleCase(doc, helper) {
|
||||
info("Checks that for an element in the main frame, without scroll nor zoom" +
|
||||
"that the returned value is similar to the returned value of getBoxQuads");
|
||||
|
||||
let node = doc.querySelector("#simple-node-with-margin-padding-border");
|
||||
|
||||
for (let region of ["content", "padding", "border", "margin"]) {
|
||||
let expected = node.getBoxQuads({
|
||||
box: region
|
||||
})[0];
|
||||
let actual = helper.getAdjustedQuads(node, region);
|
||||
|
||||
for (let boundProp of
|
||||
["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
|
||||
is(actual.bounds[boundProp], expected.bounds[boundProp],
|
||||
boundProp + " bound is equal to the one returned by getBoxQuads for " +
|
||||
region + " box");
|
||||
}
|
||||
|
||||
for (let point of ["p1", "p2", "p3", "p4"]) {
|
||||
for (let pointProp of ["x", "y", "z", "w"]) {
|
||||
is(actual[point][pointProp], expected[point][pointProp],
|
||||
point + "." + pointProp +
|
||||
" is equal to the one returned by getBoxQuads for " + region + " box");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function takesIframesOffsetsIntoAccount(doc, helper) {
|
||||
info("Checks that the quad returned for a node inside iframes that have " +
|
||||
"margins takes those offsets into account");
|
||||
|
||||
let rootIframe = doc.querySelector("iframe");
|
||||
let subIframe = rootIframe.contentDocument.querySelector("iframe");
|
||||
let innerNode = subIframe.contentDocument.querySelector("#inner-node");
|
||||
|
||||
let quad = helper.getAdjustedQuads(innerNode, "content");
|
||||
|
||||
//rootIframe margin + subIframe margin + node margin + node border + node padding
|
||||
let p1x = 10 + 10 + 10 + 10 + 10;
|
||||
is(quad.p1.x, p1x, "The inner node's p1 x position is correct");
|
||||
|
||||
// Same as p1x + the inner node width
|
||||
let p2x = p1x + 100;
|
||||
is(quad.p2.x, p2x, "The inner node's p2 x position is correct");
|
||||
}
|
||||
|
||||
function takesScrollingIntoAccount(doc, helper) {
|
||||
info("Checks that the quad returned for a node inside multiple scrolled " +
|
||||
"containers takes the scroll values into account");
|
||||
|
||||
// For info, the container being tested here is absolutely positioned at 0 0
|
||||
// to simplify asserting the coordinates
|
||||
|
||||
info("Scroll the container nodes down");
|
||||
let scrolledNode = doc.querySelector("#scrolled-node");
|
||||
scrolledNode.scrollTop = 100;
|
||||
let subScrolledNode = doc.querySelector("#sub-scrolled-node");
|
||||
subScrolledNode.scrollTop = 200;
|
||||
let innerNode = doc.querySelector("#inner-scrolled-node");
|
||||
|
||||
let quad = helper.getAdjustedQuads(innerNode, "content");
|
||||
is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling down");
|
||||
is(quad.p1.y, -300, "p1.y of the scrolled node is correct after scrolling down");
|
||||
|
||||
info("Scrolling back up");
|
||||
scrolledNode.scrollTop = 0;
|
||||
subScrolledNode.scrollTop = 0;
|
||||
|
||||
let quad = helper.getAdjustedQuads(innerNode, "content");
|
||||
is(quad.p1.x, 0, "p1.x of the scrolled node is correct after scrolling up");
|
||||
is(quad.p1.y, 0, "p1.y of the scrolled node is correct after scrolling up");
|
||||
}
|
||||
|
||||
function takesZoomIntoAccount(doc, helper) {
|
||||
info("Checks that if the page is zoomed in/out, the quad returned is correct");
|
||||
|
||||
// Hard-coding coordinates in this zoom test is a bad idea as it can vary
|
||||
// depending on the platform, so we simply test that zooming in produces a
|
||||
// bigger quad and zooming out produces a smaller quad
|
||||
|
||||
let node = doc.querySelector("#simple-node-with-margin-padding-border");
|
||||
let defaultQuad = helper.getAdjustedQuads(node);
|
||||
|
||||
info("Zoom in");
|
||||
window.FullZoom.enlarge();
|
||||
let zoomedInQuad = helper.getAdjustedQuads(node);
|
||||
|
||||
ok(zoomedInQuad.bounds.width > defaultQuad.bounds.width,
|
||||
"The zoomed in quad is bigger than the default one");
|
||||
ok(zoomedInQuad.bounds.height > defaultQuad.bounds.height,
|
||||
"The zoomed in quad is bigger than the default one");
|
||||
|
||||
info("Zoom out");
|
||||
window.FullZoom.reset();
|
||||
window.FullZoom.reduce();
|
||||
let zoomedOutQuad = helper.getAdjustedQuads(node);
|
||||
|
||||
ok(zoomedOutQuad.bounds.width < defaultQuad.bounds.width,
|
||||
"The zoomed out quad is smaller than the default one");
|
||||
ok(zoomedOutQuad.bounds.height < defaultQuad.bounds.height,
|
||||
"The zoomed out quad is smaller than the default one");
|
||||
|
||||
window.FullZoom.reset();
|
||||
}
|
@ -12,7 +12,7 @@ registerCleanupFunction(function () {
|
||||
|
||||
let LayoutHelpers = imported.LayoutHelpers;
|
||||
|
||||
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_layoutHelpers.html";
|
||||
const TEST_URI = TEST_URI_ROOT + "browser_layoutHelpers.html";
|
||||
|
||||
function test() {
|
||||
addTab(TEST_URI, function(browser, tab) {
|
||||
|
@ -12,7 +12,7 @@
|
||||
var promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
|
||||
var template = Cu.import("resource://gre/modules/devtools/Templater.jsm", {}).template;
|
||||
|
||||
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_templater_basic.html";
|
||||
const TEST_URI = TEST_URI_ROOT + "browser_templater_basic.html";
|
||||
|
||||
function test() {
|
||||
addTab(TEST_URI, function() {
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
// Tests that the developer toolbar works properly
|
||||
|
||||
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_toolbar_basic.html";
|
||||
const TEST_URI = TEST_URI_ROOT + "browser_toolbar_basic.html";
|
||||
|
||||
function test() {
|
||||
addTab(TEST_URI, function(browser, tab) {
|
||||
|
@ -4,8 +4,7 @@
|
||||
// Tests that the developer toolbar errors count works properly.
|
||||
|
||||
function test() {
|
||||
const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/" +
|
||||
"browser_toolbar_webconsole_errors_count.html";
|
||||
const TEST_URI = TEST_URI_ROOT + "browser_toolbar_webconsole_errors_count.html";
|
||||
|
||||
let gDevTools = Cu.import("resource:///modules/devtools/gDevTools.jsm",
|
||||
{}).gDevTools;
|
||||
|
@ -11,6 +11,8 @@ SimpleTest.registerCleanupFunction(() => {
|
||||
gDevTools.testing = false;
|
||||
});
|
||||
|
||||
const TEST_URI_ROOT = "http://example.com/browser/browser/devtools/shared/test/";
|
||||
|
||||
/**
|
||||
* Open a new tab at a URL and call a callback on load
|
||||
*/
|
||||
|
@ -1,389 +0,0 @@
|
||||
/* 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";
|
||||
|
||||
/**
|
||||
* The CSSTransformPreview module displays, using a <canvas> a rectangle, with
|
||||
* a given width and height and its transformed version, given a css transform
|
||||
* property and origin. It also displays arrows from/to each corner.
|
||||
*
|
||||
* It is useful to visualize how a css transform affected an element. It can
|
||||
* help debug tricky transformations. It is used today in a tooltip, and this
|
||||
* tooltip is shown when hovering over a css transform declaration in the rule
|
||||
* and computed view panels.
|
||||
*
|
||||
* TODO: For now, it multiplies matrices itself to calculate the coordinates of
|
||||
* the transformed box, but that should be removed as soon as we can get access
|
||||
* to getQuads().
|
||||
*/
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/**
|
||||
* The TransformPreview needs an element to output a canvas tag.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* let t = new CSSTransformPreviewer(myRootElement);
|
||||
* t.preview("rotate(45deg)", "top left", 200, 400);
|
||||
* t.preview("skew(19deg)", "center", 100, 500);
|
||||
* t.preview("matrix(1, -0.2, 0, 1, 0, 0)");
|
||||
* t.destroy();
|
||||
*
|
||||
* @param {nsIDOMElement} parentEl
|
||||
* Where the canvas will go
|
||||
*/
|
||||
function CSSTransformPreviewer(parentEl) {
|
||||
this.parentEl = parentEl;
|
||||
this.doc = this.parentEl.ownerDocument;
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
}
|
||||
|
||||
module.exports.CSSTransformPreviewer = CSSTransformPreviewer;
|
||||
|
||||
CSSTransformPreviewer.prototype = {
|
||||
/**
|
||||
* The preview look-and-feel can be changed using these properties
|
||||
*/
|
||||
MAX_DIM: 250,
|
||||
PAD: 5,
|
||||
ORIGINAL_FILL: "#1F303F",
|
||||
ORIGINAL_STROKE: "#B2D8FF",
|
||||
TRANSFORMED_FILL: "rgba(200, 200, 200, .5)",
|
||||
TRANSFORMED_STROKE: "#B2D8FF",
|
||||
ARROW_STROKE: "#329AFF",
|
||||
ORIGIN_STROKE: "#329AFF",
|
||||
ARROW_TIP_HEIGHT: 10,
|
||||
ARROW_TIP_WIDTH: 8,
|
||||
CORNER_SIZE_RATIO: 6,
|
||||
|
||||
/**
|
||||
* Destroy removes the canvas from the parentelement passed in the constructor
|
||||
*/
|
||||
destroy: function() {
|
||||
if (this.canvas) {
|
||||
this.parentEl.removeChild(this.canvas);
|
||||
}
|
||||
if (this._hiddenDiv) {
|
||||
this.parentEl.removeChild(this._hiddenDiv);
|
||||
}
|
||||
this.parentEl = this.canvas = this.ctx = this.doc = null;
|
||||
},
|
||||
|
||||
_createMarkup: function() {
|
||||
this.canvas = this.doc.createElementNS(HTML_NS, "canvas");
|
||||
|
||||
this.canvas.setAttribute("id", "canvas");
|
||||
this.canvas.setAttribute("width", this.MAX_DIM);
|
||||
this.canvas.setAttribute("height", this.MAX_DIM);
|
||||
this.canvas.style.position = "relative";
|
||||
this.parentEl.appendChild(this.canvas);
|
||||
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
},
|
||||
|
||||
_getComputed: function(name, value, width, height) {
|
||||
if (!this._hiddenDiv) {
|
||||
// Create a hidden element to apply the style to
|
||||
this._hiddenDiv = this.doc.createElementNS(HTML_NS, "div");
|
||||
this._hiddenDiv.style.visibility = "hidden";
|
||||
this._hiddenDiv.style.position = "absolute";
|
||||
this.parentEl.appendChild(this._hiddenDiv);
|
||||
}
|
||||
|
||||
// Camelcase the name
|
||||
name = name.replace(/-([a-z]{1})/g, (m, letter) => letter.toUpperCase());
|
||||
|
||||
// Apply width and height to make sure computation is made correctly
|
||||
this._hiddenDiv.style.width = width + "px";
|
||||
this._hiddenDiv.style.height = height + "px";
|
||||
|
||||
// Show the hidden div, apply the style, read the computed style, hide the
|
||||
// hidden div again
|
||||
this._hiddenDiv.style.display = "block";
|
||||
this._hiddenDiv.style[name] = value;
|
||||
let computed = this.doc.defaultView.getComputedStyle(this._hiddenDiv);
|
||||
let computedValue = computed[name];
|
||||
this._hiddenDiv.style.display = "none";
|
||||
|
||||
return computedValue;
|
||||
},
|
||||
|
||||
_getMatrixFromTransformString: function(transformStr) {
|
||||
let matrix = transformStr.substring(0, transformStr.length - 1).
|
||||
substring(transformStr.indexOf("(") + 1).split(",");
|
||||
|
||||
matrix.forEach(function(value, index) {
|
||||
matrix[index] = parseFloat(value, 10);
|
||||
});
|
||||
|
||||
let transformMatrix = null;
|
||||
|
||||
if (matrix.length === 6) {
|
||||
// 2d transform
|
||||
transformMatrix = [
|
||||
[matrix[0], matrix[2], matrix[4], 0],
|
||||
[matrix[1], matrix[3], matrix[5], 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 1]
|
||||
];
|
||||
} else {
|
||||
// 3d transform
|
||||
transformMatrix = [
|
||||
[matrix[0], matrix[4], matrix[8], matrix[12]],
|
||||
[matrix[1], matrix[5], matrix[9], matrix[13]],
|
||||
[matrix[2], matrix[6], matrix[10], matrix[14]],
|
||||
[matrix[3], matrix[7], matrix[11], matrix[15]]
|
||||
];
|
||||
}
|
||||
|
||||
return transformMatrix;
|
||||
},
|
||||
|
||||
_getOriginFromOriginString: function(originStr) {
|
||||
let offsets = originStr.split(" ");
|
||||
offsets.forEach(function(item, index) {
|
||||
offsets[index] = parseInt(item, 10);
|
||||
});
|
||||
|
||||
return offsets;
|
||||
},
|
||||
|
||||
_multiply: function(m1, m2) {
|
||||
let m = [];
|
||||
for (let m1Line = 0; m1Line < m1.length; m1Line++) {
|
||||
m[m1Line] = 0;
|
||||
for (let m2Col = 0; m2Col < m2.length; m2Col++) {
|
||||
m[m1Line] += m1[m1Line][m2Col] * m2[m2Col];
|
||||
}
|
||||
}
|
||||
return [m[0], m[1]];
|
||||
},
|
||||
|
||||
_getTransformedPoint: function(matrix, point, origin) {
|
||||
let pointMatrix = [point[0] - origin[0], point[1] - origin[1], 1, 1];
|
||||
return this._multiply(matrix, pointMatrix);
|
||||
},
|
||||
|
||||
_getTransformedPoints: function(matrix, rect, origin) {
|
||||
return rect.map(point => {
|
||||
let tPoint = this._getTransformedPoint(matrix, [point[0], point[1]], origin);
|
||||
return [tPoint[0] + origin[0], tPoint[1] + origin[1]];
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* For canvas to avoid anti-aliasing
|
||||
*/
|
||||
_round: x => Math.round(x) + .5,
|
||||
|
||||
_drawShape: function(points, fillStyle, strokeStyle) {
|
||||
this.ctx.save();
|
||||
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.strokeStyle = strokeStyle;
|
||||
this.ctx.fillStyle = fillStyle;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(this._round(points[0][0]), this._round(points[0][1]));
|
||||
for (var i = 1; i < points.length; i++) {
|
||||
this.ctx.lineTo(this._round(points[i][0]), this._round(points[i][1]));
|
||||
}
|
||||
this.ctx.lineTo(this._round(points[0][0]), this._round(points[0][1]));
|
||||
this.ctx.fill();
|
||||
this.ctx.stroke();
|
||||
|
||||
this.ctx.restore();
|
||||
},
|
||||
|
||||
_drawArrow: function(x1, y1, x2, y2) {
|
||||
// do not draw if the line is too small
|
||||
if (Math.abs(x2-x1) < 20 && Math.abs(y2-y1) < 20) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctx.save();
|
||||
|
||||
this.ctx.strokeStyle = this.ARROW_STROKE;
|
||||
this.ctx.fillStyle = this.ARROW_STROKE;
|
||||
this.ctx.lineWidth = 1;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(this._round(x1), this._round(y1));
|
||||
this.ctx.lineTo(this._round(x2), this._round(y2));
|
||||
this.ctx.stroke();
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.translate(x2, y2);
|
||||
let radians = Math.atan((y1 - y2) / (x1 - x2));
|
||||
radians += ((x1 >= x2) ? -90 : 90) * Math.PI / 180;
|
||||
this.ctx.rotate(radians);
|
||||
this.ctx.moveTo(0, 0);
|
||||
this.ctx.lineTo(this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
|
||||
this.ctx.lineTo(-this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
|
||||
this.ctx.closePath();
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.restore();
|
||||
},
|
||||
|
||||
_drawOrigin: function(x, y) {
|
||||
this.ctx.save();
|
||||
|
||||
this.ctx.strokeStyle = this.ORIGIN_STROKE;
|
||||
this.ctx.fillStyle = this.ORIGIN_STROKE;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
|
||||
this.ctx.stroke();
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.restore();
|
||||
},
|
||||
|
||||
/**
|
||||
* Computes the largest width and height of all the given shapes and changes
|
||||
* all of the shapes' points (by reference) so they fit into the configured
|
||||
* MAX_DIM - 2*PAD area.
|
||||
* @return {Object} A {w, h} giving the size the canvas should be
|
||||
*/
|
||||
_fitAllShapes: function(allShapes) {
|
||||
let allXs = [], allYs = [];
|
||||
for (let shape of allShapes) {
|
||||
for (let point of shape) {
|
||||
allXs.push(point[0]);
|
||||
allYs.push(point[1]);
|
||||
}
|
||||
}
|
||||
let minX = Math.min.apply(Math, allXs);
|
||||
let maxX = Math.max.apply(Math, allXs);
|
||||
let minY = Math.min.apply(Math, allYs);
|
||||
let maxY = Math.max.apply(Math, allYs);
|
||||
|
||||
let spanX = maxX - minX;
|
||||
let spanY = maxY - minY;
|
||||
let isWide = spanX > spanY;
|
||||
|
||||
let cw = isWide ? this.MAX_DIM :
|
||||
this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
|
||||
let ch = !isWide ? this.MAX_DIM :
|
||||
this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
|
||||
|
||||
let mapX = x => this.PAD + ((cw - 2 * this.PAD) / (maxX - minX)) * (x - minX);
|
||||
let mapY = y => this.PAD + ((ch - 2 * this.PAD) / (maxY - minY)) * (y - minY);
|
||||
|
||||
for (let shape of allShapes) {
|
||||
for (let point of shape) {
|
||||
point[0] = mapX(point[0]);
|
||||
point[1] = mapY(point[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return {w: cw, h: ch};
|
||||
},
|
||||
|
||||
_drawShapes: function(shape, corner, transformed, transformedCorner) {
|
||||
this._drawOriginal(shape);
|
||||
this._drawOriginalCorner(corner);
|
||||
this._drawTransformed(transformed);
|
||||
this._drawTransformedCorner(transformedCorner);
|
||||
},
|
||||
|
||||
_drawOriginal: function(points) {
|
||||
this._drawShape(points, this.ORIGINAL_FILL, this.ORIGINAL_STROKE);
|
||||
},
|
||||
|
||||
_drawTransformed: function(points) {
|
||||
this._drawShape(points, this.TRANSFORMED_FILL, this.TRANSFORMED_STROKE);
|
||||
},
|
||||
|
||||
_drawOriginalCorner: function(points) {
|
||||
this._drawShape(points, this.ORIGINAL_STROKE, this.ORIGINAL_STROKE);
|
||||
},
|
||||
|
||||
_drawTransformedCorner: function(points) {
|
||||
this._drawShape(points, this.TRANSFORMED_STROKE, this.TRANSFORMED_STROKE);
|
||||
},
|
||||
|
||||
_drawArrows: function(shape, transformed) {
|
||||
this._drawArrow(shape[0][0], shape[0][1], transformed[0][0], transformed[0][1]);
|
||||
this._drawArrow(shape[1][0], shape[1][1], transformed[1][0], transformed[1][1]);
|
||||
this._drawArrow(shape[2][0], shape[2][1], transformed[2][0], transformed[2][1]);
|
||||
this._drawArrow(shape[3][0], shape[3][1], transformed[3][0], transformed[3][1]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Draw a transform preview
|
||||
*
|
||||
* @param {String} transform
|
||||
* The css transform value as a string, as typed by the user, as long
|
||||
* as it can be computed by the browser
|
||||
* @param {String} origin
|
||||
* Same as above for the transform-origin value. Defaults to "center"
|
||||
* @param {Number} width
|
||||
* The width of the container. Defaults to 200
|
||||
* @param {Number} height
|
||||
* The height of the container. Defaults to 200
|
||||
* @return {Boolean} Whether or not the preview could be created. Will return
|
||||
* false for instance if the transform is invalid
|
||||
*/
|
||||
preview: function(transform, origin="center", width=200, height=200) {
|
||||
// Create/clear the canvas
|
||||
if (!this.canvas) {
|
||||
this._createMarkup();
|
||||
}
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Get computed versions of transform and origin
|
||||
transform = this._getComputed("transform", transform, width, height);
|
||||
if (transform && transform !== "none") {
|
||||
origin = this._getComputed("transform-origin", origin, width, height);
|
||||
|
||||
// Get the matrix, origin and width height data for the previewed element
|
||||
let originData = this._getOriginFromOriginString(origin);
|
||||
let matrixData = this._getMatrixFromTransformString(transform);
|
||||
|
||||
// Compute the original box rect and transformed box rect
|
||||
let shapePoints = [
|
||||
[0, 0],
|
||||
[width, 0],
|
||||
[width, height],
|
||||
[0, height]
|
||||
];
|
||||
let transformedPoints = this._getTransformedPoints(matrixData, shapePoints, originData);
|
||||
|
||||
// Do the same for the corner triangle shape
|
||||
let cornerSize = Math.min(shapePoints[2][1] - shapePoints[1][1],
|
||||
shapePoints[1][0] - shapePoints[0][0]) / this.CORNER_SIZE_RATIO;
|
||||
let cornerPoints = [
|
||||
[shapePoints[1][0], shapePoints[1][1]],
|
||||
[shapePoints[1][0], shapePoints[1][1] + cornerSize],
|
||||
[shapePoints[1][0] - cornerSize, shapePoints[1][1]]
|
||||
];
|
||||
let transformedCornerPoints = this._getTransformedPoints(matrixData, cornerPoints, originData);
|
||||
|
||||
// Resize points to fit everything in the canvas
|
||||
let {w, h} = this._fitAllShapes([
|
||||
shapePoints,
|
||||
transformedPoints,
|
||||
cornerPoints,
|
||||
transformedCornerPoints,
|
||||
[originData]
|
||||
]);
|
||||
|
||||
this.canvas.setAttribute("width", w);
|
||||
this.canvas.setAttribute("height", h);
|
||||
|
||||
this._drawShapes(shapePoints, cornerPoints, transformedPoints, transformedCornerPoints)
|
||||
this._drawArrows(shapePoints, transformedPoints);
|
||||
this._drawOrigin(originData[0], originData[1]);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
@ -12,7 +12,6 @@ const {Spectrum} = require("devtools/shared/widgets/Spectrum");
|
||||
const EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
const {colorUtils} = require("devtools/css-color");
|
||||
const Heritage = require("sdk/core/heritage");
|
||||
const {CSSTransformPreviewer} = require("devtools/shared/widgets/CSSTransformPreviewer");
|
||||
const {Eyedropper} = require("devtools/eyedropper/eyedropper");
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
@ -737,45 +736,6 @@ Tooltip.prototype = {
|
||||
return def.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the content of the tooltip to be the result of CSSTransformPreviewer.
|
||||
* Meaning a canvas previewing a css transformation.
|
||||
*
|
||||
* @param {String} transform
|
||||
* The CSS transform value (e.g. "rotate(45deg) translateX(50px)")
|
||||
* @param {PageStyleActor} pageStyle
|
||||
* An instance of the PageStyleActor that will be used to retrieve
|
||||
* computed styles
|
||||
* @param {NodeActor} node
|
||||
* The NodeActor for the currently selected node
|
||||
* @return A promise that resolves when the tooltip content is ready, or
|
||||
* rejects if no transform is provided or the transform is invalid
|
||||
*/
|
||||
setCssTransformContent: Task.async(function*(transform, pageStyle, node) {
|
||||
if (!transform) {
|
||||
throw "Missing transform";
|
||||
}
|
||||
|
||||
// Look into the computed styles to find the width and height and possibly
|
||||
// the origin if it hadn't been provided
|
||||
let styles = yield pageStyle.getComputed(node, {
|
||||
filter: "user",
|
||||
markMatched: false,
|
||||
onlyMatched: false
|
||||
});
|
||||
|
||||
let origin = styles["transform-origin"].value;
|
||||
let width = parseInt(styles["width"].value);
|
||||
let height = parseInt(styles["height"].value);
|
||||
|
||||
let root = this.doc.createElementNS(XHTML_NS, "div");
|
||||
let previewer = new CSSTransformPreviewer(root);
|
||||
this.content = root;
|
||||
if (!previewer.preview(transform, origin, width, height)) {
|
||||
throw "Invalid transform";
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Set the content of the tooltip to display a font family preview.
|
||||
* This is based on Lea Verou's Dablet. See https://github.com/LeaVerou/dabblet
|
||||
|
@ -26,6 +26,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
|
||||
const FILTER_CHANGED_TIMEOUT = 300;
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const TRANSFORM_HIGHLIGHTER_TYPE = "CssTransformHighlighter";
|
||||
|
||||
/**
|
||||
* Helper for long-running processes that should yield occasionally to
|
||||
@ -187,6 +188,12 @@ function CssHtmlTree(aStyleInspector, aPageStyle)
|
||||
|
||||
this._buildContextMenu();
|
||||
this.createStyleViews();
|
||||
|
||||
// Initialize the css transform highlighter if the target supports it
|
||||
let hUtils = this.styleInspector.inspector.toolbox.highlighterUtils;
|
||||
if (hUtils.hasCustomHighlighter(TRANSFORM_HIGHLIGHTER_TYPE)) {
|
||||
this._initTransformHighlighter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -513,6 +520,65 @@ CssHtmlTree.prototype = {
|
||||
win.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the css transform highlighter front, initializing it if needed
|
||||
* @param a promise that resolves to the highlighter
|
||||
*/
|
||||
getTransformHighlighter: function() {
|
||||
if (this.transformHighlighterPromise) {
|
||||
return this.transformHighlighterPromise;
|
||||
}
|
||||
|
||||
let utils = this.styleInspector.inspector.toolbox.highlighterUtils;
|
||||
this.transformHighlighterPromise =
|
||||
utils.getHighlighterByType(TRANSFORM_HIGHLIGHTER_TYPE).then(highlighter => {
|
||||
this.transformHighlighter = highlighter;
|
||||
return this.transformHighlighter;
|
||||
});
|
||||
|
||||
return this.transformHighlighterPromise;
|
||||
},
|
||||
|
||||
_initTransformHighlighter: function() {
|
||||
this.isTransformHighlighterShown = false;
|
||||
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
|
||||
this.propertyContainer.addEventListener("mousemove", this._onMouseMove, false);
|
||||
this.propertyContainer.addEventListener("mouseleave", this._onMouseLeave, false);
|
||||
},
|
||||
|
||||
_onMouseMove: function(event) {
|
||||
if (event.target === this._lastHovered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isTransformHighlighterShown) {
|
||||
this.isTransformHighlighterShown = false;
|
||||
this.getTransformHighlighter().then(highlighter => highlighter.hide());
|
||||
}
|
||||
|
||||
this._lastHovered = event.target;
|
||||
if (this._lastHovered.classList.contains("property-value")) {
|
||||
let propName = this._lastHovered.parentNode.querySelector(".property-name");
|
||||
|
||||
if (propName.textContent === "transform") {
|
||||
this.isTransformHighlighterShown = true;
|
||||
let node = this.styleInspector.inspector.selection.nodeFront;
|
||||
this.getTransformHighlighter().then(highlighter => highlighter.show(node));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onMouseLeave: function(event) {
|
||||
this._lastHovered = null;
|
||||
if (this.isTransformHighlighterShown) {
|
||||
this.isTransformHighlighterShown = false;
|
||||
this.getTransformHighlighter().then(highlighter => highlighter.hide());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Executed by the tooltip when the pointer hovers over an element of the view.
|
||||
* Used to decide whether the tooltip should be shown or not and to actually
|
||||
@ -538,12 +604,6 @@ CssHtmlTree.prototype = {
|
||||
let propValue = target;
|
||||
let propName = target.parentNode.querySelector(".property-name");
|
||||
|
||||
// Test for css transform
|
||||
if (propName.textContent === "transform") {
|
||||
return this.tooltip.setCssTransformContent(propValue.textContent,
|
||||
this.pageStyle, this.viewedElement);
|
||||
}
|
||||
|
||||
// Test for font family
|
||||
if (propName.textContent === "font-family") {
|
||||
let prop = propValue.textContent.toLowerCase();
|
||||
@ -812,6 +872,16 @@ CssHtmlTree.prototype = {
|
||||
this.tooltip.stopTogglingOnHover(this.propertyContainer);
|
||||
this.tooltip.destroy();
|
||||
|
||||
if (this.transformHighlighter) {
|
||||
this.transformHighlighter.finalize();
|
||||
this.transformHighlighter = null;
|
||||
|
||||
this.propertyContainer.removeEventListener("mousemove", this._onMouseMove, false);
|
||||
this.propertyContainer.removeEventListener("mouseleave", this._onMouseLeave, false);
|
||||
|
||||
this._lastHovered = null;
|
||||
}
|
||||
|
||||
// Remove bound listeners
|
||||
this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
|
||||
this.styleDocument.removeEventListener("copy", this._onCopy);
|
||||
|
@ -24,6 +24,7 @@ const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
|
||||
const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
|
||||
const TRANSFORM_HIGHLIGHTER_TYPE = "CssTransformHighlighter";
|
||||
|
||||
/**
|
||||
* These regular expressions are adapted from firebug's css.js, and are
|
||||
@ -1030,7 +1031,6 @@ TextProperty.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* View hierarchy mostly follows the model hierarchy.
|
||||
*
|
||||
@ -1111,6 +1111,12 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
|
||||
|
||||
this._buildContextMenu();
|
||||
this._showEmpty();
|
||||
|
||||
// Initialize the css transform highlighter if the target supports it
|
||||
let hUtils = this.inspector.toolbox.highlighterUtils;
|
||||
if (hUtils.hasCustomHighlighter(TRANSFORM_HIGHLIGHTER_TYPE)) {
|
||||
this._initTransformHighlighter();
|
||||
}
|
||||
}
|
||||
|
||||
exports.CssRuleView = CssRuleView;
|
||||
@ -1159,21 +1165,75 @@ CssRuleView.prototype = {
|
||||
popupset.appendChild(this._contextmenu);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the css transform highlighter front, initializing it if needed
|
||||
* @param a promise that resolves to the highlighter
|
||||
*/
|
||||
getTransformHighlighter: function() {
|
||||
if (this.transformHighlighterPromise) {
|
||||
return this.transformHighlighterPromise;
|
||||
}
|
||||
|
||||
let utils = this.inspector.toolbox.highlighterUtils;
|
||||
this.transformHighlighterPromise =
|
||||
utils.getHighlighterByType(TRANSFORM_HIGHLIGHTER_TYPE).then(highlighter => {
|
||||
this.transformHighlighter = highlighter;
|
||||
return this.transformHighlighter;
|
||||
});
|
||||
|
||||
return this.transformHighlighterPromise;
|
||||
},
|
||||
|
||||
_initTransformHighlighter: function() {
|
||||
this.isTransformHighlighterShown = false;
|
||||
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
|
||||
this.element.addEventListener("mousemove", this._onMouseMove, false);
|
||||
this.element.addEventListener("mouseleave", this._onMouseLeave, false);
|
||||
},
|
||||
|
||||
_onMouseMove: function(event) {
|
||||
if (event.target === this._lastHovered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isTransformHighlighterShown) {
|
||||
this.isTransformHighlighterShown = false;
|
||||
this.getTransformHighlighter().then(highlighter => highlighter.hide());
|
||||
}
|
||||
|
||||
this._lastHovered = event.target;
|
||||
let prop = event.target.textProperty;
|
||||
let isHighlightable = prop && prop.name === "transform" &&
|
||||
prop.enabled && !prop.overridden &&
|
||||
!prop.rule.pseudoElement;
|
||||
|
||||
if (isHighlightable) {
|
||||
this.isTransformHighlighterShown = true;
|
||||
let node = this.inspector.selection.nodeFront;
|
||||
this.getTransformHighlighter().then(highlighter => highlighter.show(node));
|
||||
}
|
||||
},
|
||||
|
||||
_onMouseLeave: function(event) {
|
||||
this._lastHovered = null;
|
||||
if (this.isTransformHighlighterShown) {
|
||||
this.isTransformHighlighterShown = false;
|
||||
this.getTransformHighlighter().then(highlighter => highlighter.hide());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Which type of hover-tooltip should be shown for the given element?
|
||||
* This depends on the element: does it contain an image URL, a CSS transform,
|
||||
* a font-family, ...
|
||||
* This depends on the element: does it contain a URL, a font-family, ...
|
||||
* @param {DOMNode} el The element to test
|
||||
* @return {String} The type of hover-tooltip
|
||||
*/
|
||||
_getHoverTooltipTypeForTarget: function(el) {
|
||||
let prop = el.textProperty;
|
||||
|
||||
// Test for css transform
|
||||
if (prop && prop.name === "transform") {
|
||||
return "transform";
|
||||
}
|
||||
|
||||
// Test for image
|
||||
let isUrl = el.classList.contains("theme-link") &&
|
||||
el.parentNode.classList.contains("ruleview-propertyvalue");
|
||||
@ -1218,10 +1278,6 @@ CssRuleView.prototype = {
|
||||
this.colorPicker.hide();
|
||||
}
|
||||
|
||||
if (tooltipType === "transform") {
|
||||
return this.previewTooltip.setCssTransformContent(target.textProperty.value,
|
||||
this.pageStyle, this._viewedElement);
|
||||
}
|
||||
if (tooltipType === "image") {
|
||||
let prop = target.parentNode.textProperty;
|
||||
let dim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
|
||||
@ -1465,6 +1521,16 @@ CssRuleView.prototype = {
|
||||
this.previewTooltip.destroy();
|
||||
this.colorPicker.destroy();
|
||||
|
||||
if (this.transformHighlighter) {
|
||||
this.transformHighlighter.finalize();
|
||||
this.transformHighlighter = null;
|
||||
|
||||
this.element.removeEventListener("mousemove", this._onMouseMove, false);
|
||||
this.element.removeEventListener("mouseleave", this._onMouseLeave, false);
|
||||
|
||||
this._lastHovered = null;
|
||||
}
|
||||
|
||||
if (this.element.parentNode) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
|
@ -99,4 +99,7 @@ skip-if = os == "win" && debug # bug 963492
|
||||
[browser_styleinspector_tooltip-longhand-fontfamily.js]
|
||||
[browser_styleinspector_tooltip-shorthand-fontfamily.js]
|
||||
[browser_styleinspector_tooltip-size.js]
|
||||
[browser_styleinspector_tooltip-transform.js]
|
||||
[browser_styleinspector_transform-highlighter-01.js]
|
||||
[browser_styleinspector_transform-highlighter-02.js]
|
||||
[browser_styleinspector_transform-highlighter-03.js]
|
||||
[browser_styleinspector_transform-highlighter-04.js]
|
||||
|
@ -4,8 +4,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the color picker tooltip hides when an image or transform
|
||||
// tooltip appears
|
||||
// Test that the color picker tooltip hides when an image tooltip appears
|
||||
|
||||
const PAGE_CONTENT = [
|
||||
'<style type="text/css">',
|
||||
@ -14,7 +13,6 @@ const PAGE_CONTENT = [
|
||||
' background-color: #ededed;',
|
||||
' background-image: url(chrome://global/skin/icons/warning-64.png);',
|
||||
' border: 2em solid rgba(120, 120, 120, .5);',
|
||||
' transform: skew(-16deg);',
|
||||
' }',
|
||||
'</style>',
|
||||
'Testing the color picker tooltip!'
|
||||
@ -29,7 +27,6 @@ let test = asyncTest(function*() {
|
||||
.querySelector(".ruleview-colorswatch");
|
||||
|
||||
yield testColorPickerHidesWhenImageTooltipAppears(view, swatch);
|
||||
yield testColorPickerHidesWhenTransformTooltipAppears(view, swatch);
|
||||
});
|
||||
|
||||
function* testColorPickerHidesWhenImageTooltipAppears(view, swatch) {
|
||||
@ -49,20 +46,3 @@ function* testColorPickerHidesWhenImageTooltipAppears(view, swatch) {
|
||||
|
||||
ok(true, "The color picker closed when the image preview tooltip appeared");
|
||||
}
|
||||
|
||||
function* testColorPickerHidesWhenTransformTooltipAppears(view, swatch) {
|
||||
let transformSpan = getRuleViewProperty(view, "body", "transform").valueSpan;
|
||||
let tooltip = view.colorPicker.tooltip;
|
||||
|
||||
info("Showing the color picker tooltip by clicking on the color swatch");
|
||||
let onShown = tooltip.once("shown");
|
||||
swatch.click();
|
||||
yield onShown;
|
||||
|
||||
info("Now showing the transform preview tooltip to hide the color picker");
|
||||
let onHidden = tooltip.once("hidden");
|
||||
yield assertHoverTooltipOn(view.previewTooltip, transformSpan);
|
||||
yield onHidden;
|
||||
|
||||
ok(true, "The color picker closed when the transform preview tooltip appeared");
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ const TEST_PAGE = [
|
||||
'<style type="text/css">',
|
||||
' div {',
|
||||
' width: 300px;height: 300px;border-radius: 50%;',
|
||||
' transform: skew(45deg);',
|
||||
' background: red url(chrome://global/skin/icons/warning-64.png);',
|
||||
' }',
|
||||
'</style>',
|
||||
@ -25,42 +24,10 @@ let test = asyncTest(function*() {
|
||||
|
||||
yield selectNode("div", inspector);
|
||||
|
||||
yield testTransformDimension(view);
|
||||
yield testImageDimension(view);
|
||||
yield testPickerDimension(view);
|
||||
});
|
||||
|
||||
function* testTransformDimension(ruleView) {
|
||||
info("Testing css transform tooltip dimensions");
|
||||
|
||||
let tooltip = ruleView.previewTooltip;
|
||||
let panel = tooltip.panel;
|
||||
let {valueSpan} = getRuleViewProperty(ruleView, "div", "transform");
|
||||
|
||||
// Make sure there is a hover tooltip for this property, this also will fill
|
||||
// the tooltip with its content
|
||||
yield assertHoverTooltipOn(tooltip, valueSpan);
|
||||
|
||||
info("Showing the tooltip");
|
||||
let onShown = tooltip.once("shown");
|
||||
tooltip.show();
|
||||
yield onShown;
|
||||
|
||||
// Let's not test for a specific size, but instead let's make sure it's at
|
||||
// least as big as the preview canvas
|
||||
let canvas = panel.querySelector("canvas");
|
||||
let w = canvas.width;
|
||||
let h = canvas.height;
|
||||
let panelRect = panel.getBoundingClientRect();
|
||||
|
||||
ok(panelRect.width >= w, "The panel is wide enough to show the canvas");
|
||||
ok(panelRect.height >= h, "The panel is high enough to show the canvas");
|
||||
|
||||
let onHidden = tooltip.once("hidden");
|
||||
tooltip.hide();
|
||||
yield onHidden;
|
||||
}
|
||||
|
||||
function* testImageDimension(ruleView) {
|
||||
info("Testing background-image tooltip dimensions");
|
||||
|
||||
|
@ -1,87 +0,0 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the css transform preview tooltip is shown on transform properties
|
||||
|
||||
const PAGE_CONTENT = [
|
||||
'<style type="text/css">',
|
||||
' #testElement {',
|
||||
' width: 500px;',
|
||||
' height: 300px;',
|
||||
' background: red;',
|
||||
' transform: skew(16deg);',
|
||||
' }',
|
||||
' .test-element {',
|
||||
' transform-origin: top left;',
|
||||
' transform: rotate(45deg);',
|
||||
' }',
|
||||
' div {',
|
||||
' transform: scaleX(1.5);',
|
||||
' transform-origin: bottom right;',
|
||||
' }',
|
||||
' [attr] {',
|
||||
' }',
|
||||
'</style>',
|
||||
'<div id="testElement" class="test-element" attr="value">transformed element</div>'
|
||||
].join("\n");
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
yield addTab("data:text/html,rule view css transform tooltip test");
|
||||
content.document.body.innerHTML = PAGE_CONTENT;
|
||||
let {toolbox, inspector, view} = yield openRuleView();
|
||||
|
||||
info("Selecting the test node");
|
||||
yield selectNode("#testElement", inspector);
|
||||
|
||||
info("Checking that transforms tooltips are shown in various rule-view properties");
|
||||
for (let selector of [".test-element", "div", "#testElement"]) {
|
||||
yield testTransformTooltipOnSelector(view, selector);
|
||||
}
|
||||
|
||||
info("Checking that the transform tooltip doesn't appear for invalid transforms");
|
||||
yield testTransformTooltipNotShownOnInvalidTransform(view);
|
||||
|
||||
info("Checking transforms in the computed-view");
|
||||
let {view} = yield openComputedView();
|
||||
yield testTransformTooltipOnComputedView(view);
|
||||
});
|
||||
|
||||
function* testTransformTooltipOnSelector(view, selector) {
|
||||
info("Testing that a transform tooltip appears on transform in " + selector);
|
||||
|
||||
let {valueSpan} = getRuleViewProperty(view, selector, "transform");
|
||||
ok(valueSpan, "The transform property was found");
|
||||
yield assertHoverTooltipOn(view.previewTooltip, valueSpan);
|
||||
|
||||
// The transform preview is canvas, so there's not much we can test, so for
|
||||
// now, let's just be happy with the fact that the tooltips is shown!
|
||||
ok(true, "Tooltip shown on the transform property in " + selector);
|
||||
}
|
||||
|
||||
function* testTransformTooltipNotShownOnInvalidTransform(view) {
|
||||
let ruleEditor;
|
||||
for (let rule of view._elementStyle.rules) {
|
||||
if (rule.matchedSelectors[0] === "[attr]") {
|
||||
ruleEditor = rule.editor;
|
||||
}
|
||||
}
|
||||
ruleEditor.addProperty("transform", "muchTransform(suchAngle)", "");
|
||||
|
||||
let {valueSpan} = getRuleViewProperty(view, "[attr]", "transform");
|
||||
let isValid = yield isHoverTooltipTarget(view.previewTooltip, valueSpan);
|
||||
ok(!isValid, "The tooltip did not appear on hover of an invalid transform value");
|
||||
}
|
||||
|
||||
function* testTransformTooltipOnComputedView(view) {
|
||||
info("Testing that a transform tooltip appears in the computed view too");
|
||||
|
||||
let {valueSpan} = getComputedViewProperty(view, "transform");
|
||||
yield assertHoverTooltipOn(view.tooltip, valueSpan);
|
||||
|
||||
// The transform preview is canvas, so there's not much we can test, so for
|
||||
// now, let's just be happy with the fact that the tooltips is shown!
|
||||
ok(true, "Tooltip shown on the computed transform property");
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the css transform highlighter is created only when asked
|
||||
|
||||
const PAGE_CONTENT = [
|
||||
'<style type="text/css">',
|
||||
' body {',
|
||||
' transform: skew(16deg);',
|
||||
' }',
|
||||
'</style>',
|
||||
'Test the css transform highlighter'
|
||||
].join("\n");
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
yield addTab("data:text/html," + PAGE_CONTENT);
|
||||
|
||||
let {view: rView} = yield openRuleView();
|
||||
|
||||
ok(!rView.transformHighlighter, "No highlighter exists in the rule-view");
|
||||
let h = yield rView.getTransformHighlighter();
|
||||
ok(rView.transformHighlighter, "The highlighter has been created in the rule-view");
|
||||
is(h, rView.transformHighlighter, "The right highlighter has been created");
|
||||
let h2 = yield rView.getTransformHighlighter();
|
||||
is(h, h2, "The same instance of highlighter is returned everytime in the rule-view");
|
||||
|
||||
let {view: cView} = yield openComputedView();
|
||||
|
||||
ok(!cView.transformHighlighter, "No highlighter exists in the computed-view");
|
||||
let h = yield cView.getTransformHighlighter();
|
||||
ok(cView.transformHighlighter, "The highlighter has been created in the computed-view");
|
||||
is(h, cView.transformHighlighter, "The right highlighter has been created");
|
||||
let h2 = yield cView.getTransformHighlighter();
|
||||
is(h, h2, "The same instance of highlighter is returned everytime in the computed-view");
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the css transform highlighter is created when hovering over a
|
||||
// transform property
|
||||
|
||||
const PAGE_CONTENT = [
|
||||
'<style type="text/css">',
|
||||
' body {',
|
||||
' transform: skew(16deg);',
|
||||
' color: yellow;',
|
||||
' }',
|
||||
'</style>',
|
||||
'Test the css transform highlighter'
|
||||
].join("\n");
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
yield addTab("data:text/html," + PAGE_CONTENT);
|
||||
|
||||
let {view: rView} = yield openRuleView();
|
||||
ok(!rView.transformHighlighter, "No highlighter exists in the rule-view (1)");
|
||||
|
||||
info("Faking a mousemove on a non-transform property");
|
||||
let {valueSpan} = getRuleViewProperty(rView, "body", "color");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
ok(!rView.transformHighlighter, "No highlighter exists in the rule-view (2)");
|
||||
ok(!rView.transformHighlighterPromise, "No highlighter is being initialized");
|
||||
|
||||
info("Faking a mousemove on a transform property");
|
||||
let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
ok(rView.transformHighlighterPromise, "The highlighter is being initialized");
|
||||
let h = yield rView.transformHighlighterPromise;
|
||||
is(h, rView.transformHighlighter, "The initialized highlighter is the right one");
|
||||
|
||||
let {view: cView} = yield openComputedView();
|
||||
ok(!cView.transformHighlighter, "No highlighter exists in the computed-view (1)");
|
||||
|
||||
info("Faking a mousemove on a non-transform property");
|
||||
let {valueSpan} = getComputedViewProperty(cView, "color");
|
||||
cView._onMouseMove({target: valueSpan});
|
||||
ok(!cView.transformHighlighter, "No highlighter exists in the computed-view (2)");
|
||||
ok(!cView.transformHighlighterPromise, "No highlighter is being initialized");
|
||||
|
||||
info("Faking a mousemove on a transform property");
|
||||
let {valueSpan} = getComputedViewProperty(cView, "transform");
|
||||
cView._onMouseMove({target: valueSpan});
|
||||
ok(cView.transformHighlighterPromise, "The highlighter is being initialized");
|
||||
let h = yield cView.transformHighlighterPromise;
|
||||
is(h, cView.transformHighlighter, "The initialized highlighter is the right one");
|
||||
});
|
@ -0,0 +1,89 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the css transform highlighter is shown when hovering over transform
|
||||
// properties
|
||||
|
||||
// Note that in this test, we mock the highlighter front, merely testing the
|
||||
// behavior of the style-inspector UI for now
|
||||
|
||||
const PAGE_CONTENT = [
|
||||
'<style type="text/css">',
|
||||
' html {',
|
||||
' transform: scale(.9);',
|
||||
' }',
|
||||
' body {',
|
||||
' transform: skew(16deg);',
|
||||
' color: purple;',
|
||||
' }',
|
||||
'</style>',
|
||||
'Test the css transform highlighter'
|
||||
].join("\n");
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
yield addTab("data:text/html," + PAGE_CONTENT);
|
||||
|
||||
let {inspector, view: rView} = yield openRuleView();
|
||||
|
||||
// Mock the highlighter front to get the reference of the NodeFront
|
||||
let HighlighterFront = {
|
||||
isShown: false,
|
||||
nodeFront: null,
|
||||
nbOfTimesShown: 0,
|
||||
show: function(nodeFront) {
|
||||
this.nodeFront = nodeFront;
|
||||
this.isShown = true;
|
||||
this.nbOfTimesShown ++;
|
||||
},
|
||||
hide: function() {
|
||||
this.nodeFront = null;
|
||||
this.isShown = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Inject the mock highlighter in the rule-view
|
||||
rView.transformHighlighterPromise = {
|
||||
then: function(cb) {
|
||||
cb(HighlighterFront);
|
||||
}
|
||||
};
|
||||
|
||||
let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
|
||||
|
||||
info("Checking that the HighlighterFront's show/hide methods are called");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
ok(HighlighterFront.isShown, "The highlighter is shown");
|
||||
rView._onMouseLeave();
|
||||
ok(!HighlighterFront.isShown, "The highlighter is hidden");
|
||||
|
||||
info("Checking that hovering several times over the same property doesn't" +
|
||||
" show the highlighter several times");
|
||||
let nb = HighlighterFront.nbOfTimesShown;
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
is(HighlighterFront.nbOfTimesShown, nb + 1,
|
||||
"The highlighter was shown once, after several mousemove");
|
||||
|
||||
info("Checking that the right NodeFront reference is passed");
|
||||
yield selectNode(content.document.documentElement, inspector);
|
||||
let {valueSpan} = getRuleViewProperty(rView, "html", "transform");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
is(HighlighterFront.nodeFront.tagName, "HTML",
|
||||
"The right NodeFront is passed to the highlighter (1)");
|
||||
|
||||
yield selectNode("body", inspector);
|
||||
let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
is(HighlighterFront.nodeFront.tagName, "BODY",
|
||||
"The right NodeFront is passed to the highlighter (2)");
|
||||
|
||||
info("Checking that the highlighter gets hidden when hovering a non-transform property");
|
||||
let {valueSpan} = getRuleViewProperty(rView, "body", "color");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
ok(!HighlighterFront.isShown, "The highlighter is hidden");
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the css transform highlighter is shown only when hovering over a
|
||||
// transform declaration that isn't overriden or disabled
|
||||
|
||||
// Note that unlike the other browser_styleinspector_transform-highlighter-N.js
|
||||
// tests, this one only tests the rule-view as only this view features disabled
|
||||
// and overriden properties
|
||||
|
||||
const PAGE_CONTENT = [
|
||||
'<style type="text/css">',
|
||||
' div {',
|
||||
' background: purple;',
|
||||
' width:300px;height:300px;',
|
||||
' transform: rotate(16deg);',
|
||||
' }',
|
||||
' .test {',
|
||||
' transform: skew(25deg);',
|
||||
' }',
|
||||
'</style>',
|
||||
'<div class="test"></div>'
|
||||
].join("\n");
|
||||
|
||||
let test = asyncTest(function*() {
|
||||
yield addTab("data:text/html," + PAGE_CONTENT);
|
||||
|
||||
let {view: rView, inspector} = yield openRuleView();
|
||||
yield selectNode(".test", inspector);
|
||||
|
||||
info("Faking a mousemove on the overriden property");
|
||||
let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
ok(!rView.transformHighlighter, "No highlighter was created for the overriden property");
|
||||
ok(!rView.transformHighlighterPromise, "And no highlighter is being initialized either");
|
||||
|
||||
info("Disabling the applied property");
|
||||
let classRuleEditor = rView.element.children[1]._ruleEditor;
|
||||
let propEditor = classRuleEditor.rule.textProps[0].editor;
|
||||
propEditor.enable.click();
|
||||
yield classRuleEditor.rule._applyingModifications;
|
||||
|
||||
info("Faking a mousemove on the disabled property");
|
||||
let {valueSpan} = getRuleViewProperty(rView, ".test", "transform");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
ok(!rView.transformHighlighter, "No highlighter was created for the disabled property");
|
||||
ok(!rView.transformHighlighterPromise, "And no highlighter is being initialized either");
|
||||
|
||||
info("Faking a mousemove on the now unoverriden property");
|
||||
let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
|
||||
rView._onMouseMove({target: valueSpan});
|
||||
ok(rView.transformHighlighterPromise, "The highlighter is being initialized now");
|
||||
let h = yield rView.transformHighlighterPromise;
|
||||
is(h, rView.transformHighlighter, "The initialized highlighter is the right one");
|
||||
});
|
@ -5,6 +5,7 @@
|
||||
%endif
|
||||
|
||||
/* Box model highlighter */
|
||||
|
||||
svg|g.box-model-container {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@ -107,3 +108,23 @@ html|*.highlighter-nodeinfobar-dimensions {
|
||||
.highlighter-nodeinfobar-container[hide-arrow] > .highlighter-nodeinfobar {
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
/* Css transform highlighter */
|
||||
|
||||
svg|polygon.css-transform-transformed {
|
||||
fill: #80d4ff;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
svg|polygon.css-transform-untransformed {
|
||||
fill: #66cc52;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
svg|polygon.css-transform-transformed,
|
||||
svg|polygon.css-transform-untransformed,
|
||||
svg|line.css-transform-line {
|
||||
stroke: #08C;
|
||||
stroke-dasharray: 5 3;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
@ -25,16 +25,18 @@ LayoutHelpers.prototype = {
|
||||
|
||||
/**
|
||||
* Get box quads adjusted for iframes and zoom level.
|
||||
*
|
||||
|
||||
* @param {DOMNode} node
|
||||
* The node for which we are to get the box model region quads
|
||||
* @param {String} region
|
||||
* The box model region to return:
|
||||
* "content", "padding", "border" or "margin"
|
||||
* @return {Object} An object that has the same structure as one quad returned
|
||||
* by getBoxQuads
|
||||
*/
|
||||
getAdjustedQuads: function(node, region) {
|
||||
if (!node) {
|
||||
return;
|
||||
if (!node || !node.getBoxQuads) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let [quads] = node.getBoxQuads({
|
||||
@ -42,10 +44,10 @@ LayoutHelpers.prototype = {
|
||||
});
|
||||
|
||||
if (!quads) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
let [xOffset, yOffset] = this._getNodeOffsets(node);
|
||||
let [xOffset, yOffset] = this.getFrameOffsets(node);
|
||||
let scale = this.calculateScale(node);
|
||||
|
||||
return {
|
||||
@ -86,6 +88,12 @@ LayoutHelpers.prototype = {
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current zoom factor applied to the container window of a given node
|
||||
* @param {DOMNode}
|
||||
* The node for which the zoom factor should be calculated
|
||||
* @return {Number}
|
||||
*/
|
||||
calculateScale: function(node) {
|
||||
let win = node.ownerDocument.defaultView;
|
||||
let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
@ -97,12 +105,14 @@ LayoutHelpers.prototype = {
|
||||
* Compute the absolute position and the dimensions of a node, relativalely
|
||||
* to the root window.
|
||||
*
|
||||
* @param nsIDOMNode aNode
|
||||
* @param {DOMNode} aNode
|
||||
* a DOM element to get the bounds for
|
||||
* @param nsIWindow aContentWindow
|
||||
* @param {DOMWindow} aContentWindow
|
||||
* the content window holding the node
|
||||
* @return {Object}
|
||||
* A rect object with the {top, left, width, height} properties
|
||||
*/
|
||||
getRect: function LH_getRect(aNode, aContentWindow) {
|
||||
getRect: function(aNode, aContentWindow) {
|
||||
let frameWin = aNode.ownerDocument.defaultView;
|
||||
let clientRect = aNode.getBoundingClientRect();
|
||||
|
||||
@ -115,7 +125,6 @@ LayoutHelpers.prototype = {
|
||||
|
||||
// We iterate through all the parent windows.
|
||||
while (true) {
|
||||
|
||||
// Are we in the top-level window?
|
||||
if (this.isTopLevelWindow(frameWin)) {
|
||||
break;
|
||||
@ -149,15 +158,15 @@ LayoutHelpers.prototype = {
|
||||
* suitable API for determining the offset between the iframe's content and
|
||||
* its bounding client rect. Bug 626359 should provide us with such an API.
|
||||
*
|
||||
* @param aIframe
|
||||
* @param {DOMNode} aIframe
|
||||
* The iframe.
|
||||
* @returns array [offsetTop, offsetLeft]
|
||||
* offsetTop is the distance from the top of the iframe and the
|
||||
* top of the content document.
|
||||
* offsetLeft is the distance from the left of the iframe and the
|
||||
* left of the content document.
|
||||
* @return {Array} [offsetTop, offsetLeft]
|
||||
* offsetTop is the distance from the top of the iframe and the top of
|
||||
* the content document.
|
||||
* offsetLeft is the distance from the left of the iframe and the left
|
||||
* of the content document.
|
||||
*/
|
||||
getIframeContentOffset: function LH_getIframeContentOffset(aIframe) {
|
||||
getIframeContentOffset: function(aIframe) {
|
||||
let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
|
||||
|
||||
// In some cases, the computed style is null
|
||||
@ -178,12 +187,14 @@ LayoutHelpers.prototype = {
|
||||
* Find an element from the given coordinates. This method descends through
|
||||
* frames to find the element the user clicked inside frames.
|
||||
*
|
||||
* @param DOMDocument aDocument the document to look into.
|
||||
* @param integer aX
|
||||
* @param integer aY
|
||||
* @returns Node|null the element node found at the given coordinates.
|
||||
* @param {DOMDocument} aDocument the document to look into.
|
||||
* @param {Number} aX
|
||||
* @param {Number} aY
|
||||
* @return {DOMNode}
|
||||
* the element node found at the given coordinates, or null if no node
|
||||
* was found
|
||||
*/
|
||||
getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY) {
|
||||
getElementFromPoint: function(aDocument, aX, aY) {
|
||||
let node = aDocument.elementFromPoint(aX, aY);
|
||||
if (node && node.contentDocument) {
|
||||
if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
|
||||
@ -214,10 +225,12 @@ LayoutHelpers.prototype = {
|
||||
/**
|
||||
* Scroll the document so that the element "elem" appears in the viewport.
|
||||
*
|
||||
* @param Element elem the element that needs to appear in the viewport.
|
||||
* @param bool centered true if you want it centered, false if you want it to
|
||||
* appear on the top of the viewport. It is true by default, and that is
|
||||
* usually what you want.
|
||||
* @param {DOMNode} elem
|
||||
* The element that needs to appear in the viewport.
|
||||
* @param {Boolean} centered
|
||||
* true if you want it centered, false if you want it to appear on the
|
||||
* top of the viewport. It is true by default, and that is usually what
|
||||
* you want.
|
||||
*/
|
||||
scrollIntoViewIfNeeded: function(elem, centered) {
|
||||
// We want to default to centering the element in the page,
|
||||
@ -293,10 +306,10 @@ LayoutHelpers.prototype = {
|
||||
* Check if a node and its document are still alive
|
||||
* and attached to the window.
|
||||
*
|
||||
* @param aNode
|
||||
* @param {DOMNode} aNode
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isNodeConnected: function LH_isNodeConnected(aNode)
|
||||
{
|
||||
isNodeConnected: function(aNode) {
|
||||
try {
|
||||
let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView &&
|
||||
!(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) &
|
||||
@ -310,8 +323,11 @@ LayoutHelpers.prototype = {
|
||||
|
||||
/**
|
||||
* like win.parent === win, but goes through mozbrowsers and mozapps iframes.
|
||||
*
|
||||
* @param {DOMWindow} win
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isTopLevelWindow: function LH_isTopLevelWindow(win) {
|
||||
isTopLevelWindow: function(win) {
|
||||
let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShell);
|
||||
@ -321,6 +337,9 @@ LayoutHelpers.prototype = {
|
||||
|
||||
/**
|
||||
* Check a window is part of the top level window.
|
||||
*
|
||||
* @param {DOMWindow} win
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isIncludedInTopLevelWindow: function LH_isIncludedInTopLevelWindow(win) {
|
||||
if (this.isTopLevelWindow(win)) {
|
||||
@ -337,8 +356,11 @@ LayoutHelpers.prototype = {
|
||||
|
||||
/**
|
||||
* like win.parent, but goes through mozbrowsers and mozapps iframes.
|
||||
*
|
||||
* @param {DOMWindow} win
|
||||
* @return {DOMWindow}
|
||||
*/
|
||||
getParentWindow: function LH_getParentWindow(win) {
|
||||
getParentWindow: function(win) {
|
||||
if (this.isTopLevelWindow(win)) {
|
||||
return null;
|
||||
}
|
||||
@ -358,10 +380,12 @@ LayoutHelpers.prototype = {
|
||||
/**
|
||||
* like win.frameElement, but goes through mozbrowsers and mozapps iframes.
|
||||
*
|
||||
* @param DOMWindow win The window to get the frame for
|
||||
* @return DOMElement The element in which the window is embedded.
|
||||
* @param {DOMWindow} win
|
||||
* The window to get the frame for
|
||||
* @return {DOMNode}
|
||||
* The element in which the window is embedded.
|
||||
*/
|
||||
getFrameElement: function LH_getFrameElement(win) {
|
||||
getFrameElement: function(win) {
|
||||
if (this.isTopLevelWindow(win)) {
|
||||
return null;
|
||||
}
|
||||
@ -374,12 +398,14 @@ LayoutHelpers.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the x and y offsets for a node taking iframes into account.
|
||||
* Get the x/y offsets for of all the parent frames of a given node
|
||||
*
|
||||
* @param {DOMNode} node
|
||||
* The node for which we are to get the offset
|
||||
* @return {Array}
|
||||
* The frame offset [x, y]
|
||||
*/
|
||||
_getNodeOffsets: function(node) {
|
||||
getFrameOffsets: function(node) {
|
||||
let xOffset = 0;
|
||||
let yOffset = 0;
|
||||
let frameWin = node.ownerDocument.defaultView;
|
||||
@ -412,4 +438,61 @@ LayoutHelpers.prototype = {
|
||||
|
||||
return [xOffset * scale, yOffset * scale];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the 4 bounding points for a node taking iframes into account.
|
||||
* Note that for transformed nodes, this will return the untransformed bound.
|
||||
*
|
||||
* @param {DOMNode} node
|
||||
* @return {Object}
|
||||
* An object with p1,p2,p3,p4 properties being {x,y} objects
|
||||
*/
|
||||
getNodeBounds: function(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scale = this.calculateScale(node);
|
||||
|
||||
// Find out the offset of the node in its current frame
|
||||
let offsetLeft = 0;
|
||||
let offsetTop = 0;
|
||||
let el = node;
|
||||
while (el && el.parentNode) {
|
||||
offsetLeft += el.offsetLeft;
|
||||
offsetTop += el.offsetTop;
|
||||
el = el.offsetParent;
|
||||
}
|
||||
|
||||
// Also take scrolled containers into account
|
||||
let el = node;
|
||||
while (el && el.parentNode) {
|
||||
if (el.scrollTop) {
|
||||
offsetTop -= el.scrollTop;
|
||||
}
|
||||
if (el.scrollLeft) {
|
||||
offsetLeft -= el.scrollLeft;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
|
||||
// And add the potential frame offset if the node is nested
|
||||
let [xOffset, yOffset] = this.getFrameOffsets(node);
|
||||
xOffset += offsetLeft;
|
||||
yOffset += offsetTop;
|
||||
|
||||
xOffset *= scale;
|
||||
yOffset *= scale;
|
||||
|
||||
// Get the width and height
|
||||
let width = node.offsetWidth * scale;
|
||||
let height = node.offsetHeight * scale;
|
||||
|
||||
return {
|
||||
p1: {x: xOffset, y: yOffset},
|
||||
p2: {x: xOffset + width, y: yOffset},
|
||||
p3: {x: xOffset + width, y: yOffset + height},
|
||||
p4: {x: xOffset, y: yOffset + height}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ const Services = require("Services");
|
||||
const protocol = require("devtools/server/protocol");
|
||||
const {Arg, Option, method} = protocol;
|
||||
const events = require("sdk/event/core");
|
||||
const Heritage = require("sdk/core/heritage");
|
||||
|
||||
const EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
const GUIDE_STROKE_WIDTH = 1;
|
||||
@ -28,18 +29,42 @@ const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const HIGHLIGHTER_PICKED_TIMER = 1000;
|
||||
const INFO_BAR_OFFSET = 5;
|
||||
// The minimum distance a line should be before it has an arrow marker-end
|
||||
const ARROW_LINE_MIN_DISTANCE = 10;
|
||||
|
||||
// All possible highlighter classes
|
||||
let HIGHLIGHTER_CLASSES = exports.HIGHLIGHTER_CLASSES = {
|
||||
"BoxModelHighlighter": BoxModelHighlighter,
|
||||
"CssTransformHighlighter": CssTransformHighlighter
|
||||
};
|
||||
|
||||
/**
|
||||
* The HighlighterActor is the server-side entry points for any tool that wishes
|
||||
* to highlight elements in the content document.
|
||||
* The Highlighter is the server-side entry points for any tool that wishes to
|
||||
* highlight elements in some way in the content document.
|
||||
*
|
||||
* The highlighter can be retrieved via the inspector's getHighlighter method.
|
||||
* A little bit of vocabulary:
|
||||
* - <something>HighlighterActor classes are the actors that can be used from
|
||||
* the client. They do very little else than instantiate a given
|
||||
* <something>Highlighter and use it to highlight elements.
|
||||
* - <something>Highlighter classes aren't actors, they're just JS classes that
|
||||
* know how to create and attach the actual highlighter elements on top of the
|
||||
* content
|
||||
*
|
||||
* The most used highlighter actor is the HighlighterActor which can be
|
||||
* conveniently retrieved via the InspectorActor's 'getHighlighter' method.
|
||||
* The InspectorActor will always return the same instance of
|
||||
* HighlighterActor if asked several times and this instance is used in the
|
||||
* toolbox to highlighter elements's box-model from the markup-view, layout-view,
|
||||
* console, debugger, ... as well as select elements with the pointer (pick).
|
||||
*
|
||||
* Other types of highlighter actors exist and can be accessed via the
|
||||
* InspectorActor's 'getHighlighterByType' method.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The HighlighterActor class
|
||||
*/
|
||||
let HighlighterActor = protocol.ActorClass({
|
||||
let HighlighterActor = exports.HighlighterActor = protocol.ActorClass({
|
||||
typeName: "highlighter",
|
||||
|
||||
initialize: function(inspector, autohide) {
|
||||
@ -53,7 +78,7 @@ let HighlighterActor = protocol.ActorClass({
|
||||
this._highlighterReady = this._highlighterReady.bind(this);
|
||||
this._highlighterHidden = this._highlighterHidden.bind(this);
|
||||
|
||||
if (this._supportsBoxModelHighlighter()) {
|
||||
if (supportXULBasedHighlighter(this._tabActor)) {
|
||||
this._boxModelHighlighter =
|
||||
new BoxModelHighlighter(this._tabActor, this._inspector);
|
||||
|
||||
@ -66,18 +91,6 @@ let HighlighterActor = protocol.ActorClass({
|
||||
|
||||
get conn() this._inspector && this._inspector.conn,
|
||||
|
||||
/**
|
||||
* Can the host support the box model highlighter which requires a parent
|
||||
* XUL node to attach itself.
|
||||
*/
|
||||
_supportsBoxModelHighlighter: function() {
|
||||
// Note that <browser>s on Fennec also have a XUL parentNode but the box
|
||||
// model highlighter doesn't display correctly on Fennec (bug 993190)
|
||||
return this._tabActor.browser &&
|
||||
!!this._tabActor.browser.parentNode &&
|
||||
Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
protocol.Actor.prototype.destroy.call(this);
|
||||
if (this._boxModelHighlighter) {
|
||||
@ -103,7 +116,7 @@ let HighlighterActor = protocol.ActorClass({
|
||||
* all options may be supported by all types of highlighters.
|
||||
*/
|
||||
showBoxModel: method(function(node, options={}) {
|
||||
if (node && this._isNodeValidForHighlighting(node.rawNode)) {
|
||||
if (node && isNodeValid(node.rawNode)) {
|
||||
this._boxModelHighlighter.show(node.rawNode, options);
|
||||
} else {
|
||||
this._boxModelHighlighter.hide();
|
||||
@ -115,25 +128,6 @@ let HighlighterActor = protocol.ActorClass({
|
||||
}
|
||||
}),
|
||||
|
||||
_isNodeValidForHighlighting: function(node) {
|
||||
// Is it null or dead?
|
||||
let isNotDead = node && !Cu.isDeadWrapper(node);
|
||||
|
||||
// Is it connected to the document?
|
||||
let isConnected = false;
|
||||
try {
|
||||
let doc = node.ownerDocument;
|
||||
isConnected = (doc && doc.defaultView && doc.documentElement.contains(node));
|
||||
} catch (e) {
|
||||
// "can't access dead object" error
|
||||
}
|
||||
|
||||
// Is it an element node
|
||||
let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE;
|
||||
|
||||
return isNotDead && isConnected && isElementNode;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the box model highlighting if it was shown before
|
||||
*/
|
||||
@ -258,12 +252,190 @@ let HighlighterActor = protocol.ActorClass({
|
||||
})
|
||||
});
|
||||
|
||||
exports.HighlighterActor = HighlighterActor;
|
||||
let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
|
||||
|
||||
/**
|
||||
* The HighlighterFront class
|
||||
* A generic highlighter actor class that instantiate a highlighter given its
|
||||
* type name and allows to show/hide it.
|
||||
*/
|
||||
let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
|
||||
let CustomHighlighterActor = exports.CustomHighlighterActor = protocol.ActorClass({
|
||||
typeName: "customhighlighter",
|
||||
|
||||
/**
|
||||
* Create a highlighter instance given its typename
|
||||
* The typename must be one of HIGHLIGHTER_CLASSES and the class must
|
||||
* implement constructor(tab, inspector), show(node), hide(), destroy()
|
||||
*/
|
||||
initialize: function(inspector, typeName) {
|
||||
protocol.Actor.prototype.initialize.call(this, null);
|
||||
|
||||
this._inspector = inspector;
|
||||
|
||||
let constructor = HIGHLIGHTER_CLASSES[typeName];
|
||||
if (!constructor) {
|
||||
throw new Error(typeName + " isn't a valid highlighter class (" +
|
||||
Object.keys(HIGHLIGHTER_CLASSES) + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// The assumption is that all custom highlighters need a XUL parent in the
|
||||
// browser to append their elements
|
||||
if (supportXULBasedHighlighter(inspector.tabActor)) {
|
||||
this._highlighter = new constructor(inspector.tabActor, inspector);
|
||||
}
|
||||
},
|
||||
|
||||
get conn() this._inspector && this._inspector.conn,
|
||||
|
||||
destroy: function() {
|
||||
protocol.Actor.prototype.destroy.call(this);
|
||||
this.finalize();
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the highlighter on a given NodeActor.
|
||||
* @param NodeActor The node to be highlighted
|
||||
*/
|
||||
show: method(function(node) {
|
||||
if (!node || !isNodeValid(node.rawNode) || !this._highlighter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._highlighter.show(node.rawNode);
|
||||
}, {
|
||||
request: {
|
||||
node: Arg(0, "domnode")
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Hide the highlighter if it was shown before
|
||||
*/
|
||||
hide: method(function() {
|
||||
if (this._highlighter) {
|
||||
this._highlighter.hide();
|
||||
}
|
||||
}, {
|
||||
request: {}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Kill this actor. This method is called automatically just before the actor
|
||||
* is destroyed.
|
||||
*/
|
||||
finalize: method(function() {
|
||||
if (this._highlighter) {
|
||||
this._highlighter.destroy();
|
||||
this._highlighter = null;
|
||||
}
|
||||
}, {
|
||||
oneway: true
|
||||
})
|
||||
});
|
||||
|
||||
let CustomHighlighterFront = protocol.FrontClass(CustomHighlighterActor, {});
|
||||
|
||||
/**
|
||||
* Parent class for XUL-based complex highlighter that are inserted in the
|
||||
* parent browser structure
|
||||
*/
|
||||
function XULBasedHighlighter(tabActor, inspector) {
|
||||
this._inspector = inspector;
|
||||
|
||||
this.browser = tabActor.browser;
|
||||
this.win = tabActor.window;
|
||||
this.chromeDoc = this.browser.ownerDocument;
|
||||
this.currentNode = null;
|
||||
|
||||
this.update = this.update.bind(this);
|
||||
}
|
||||
|
||||
XULBasedHighlighter.prototype = {
|
||||
/**
|
||||
* Show the highlighter on a given node
|
||||
* @param {DOMNode} node
|
||||
*/
|
||||
show: function(node) {
|
||||
if (!isNodeValid(node) || node === this.currentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._detachPageListeners();
|
||||
this.currentNode = node;
|
||||
this._attachPageListeners();
|
||||
this._show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the highlighter
|
||||
*/
|
||||
hide: function() {
|
||||
if (!isNodeValid(this.currentNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._hide();
|
||||
this._detachPageListeners();
|
||||
this.currentNode = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the highlighter while shown
|
||||
*/
|
||||
update: function() {
|
||||
if (isNodeValid(this.currentNode)) {
|
||||
this._update();
|
||||
}
|
||||
},
|
||||
|
||||
_show: function() {
|
||||
// To be implemented by sub classes
|
||||
// When called, sub classes should actually show the highlighter for
|
||||
// this.currentNode
|
||||
},
|
||||
|
||||
_update: function() {
|
||||
// To be implemented by sub classes
|
||||
// When called, sub classes should update the highlighter shown for
|
||||
// this.currentNode
|
||||
// This is called as a result of a page scroll, zoom or repaint
|
||||
},
|
||||
|
||||
_hide: function() {
|
||||
// To be implemented by sub classes
|
||||
// When called, sub classes should actually hide the highlighter
|
||||
},
|
||||
|
||||
/**
|
||||
* Listen to changes on the content page to update the highlighter
|
||||
*/
|
||||
_attachPageListeners: function() {
|
||||
if (isNodeValid(this.currentNode)) {
|
||||
let win = this.currentNode.ownerDocument.defaultView;
|
||||
this.browser.addEventListener("MozAfterPaint", this.update);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop listening to page changes
|
||||
*/
|
||||
_detachPageListeners: function() {
|
||||
if (isNodeValid(this.currentNode)) {
|
||||
let win = this.currentNode.ownerDocument.defaultView;
|
||||
this.browser.removeEventListener("MozAfterPaint", this.update);
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.hide();
|
||||
|
||||
this.win = null;
|
||||
this.browser = null;
|
||||
this.chromeDoc = null;
|
||||
this._inspector = null;
|
||||
this.currentNode = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The BoxModelHighlighter is the class that actually draws the the box model
|
||||
@ -308,26 +480,13 @@ let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
|
||||
* </stack>
|
||||
*/
|
||||
function BoxModelHighlighter(tabActor, inspector) {
|
||||
this.browser = tabActor.browser;
|
||||
this.win = tabActor.window;
|
||||
this.chromeDoc = this.browser.ownerDocument;
|
||||
this.chromeWin = this.chromeDoc.defaultView;
|
||||
this._inspector = inspector;
|
||||
|
||||
XULBasedHighlighter.call(this, tabActor, inspector);
|
||||
this.layoutHelpers = new LayoutHelpers(this.win);
|
||||
this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
|
||||
|
||||
this.transitionDisabler = null;
|
||||
this.pageEventsMuter = null;
|
||||
this._update = this._update.bind(this);
|
||||
this.handleEvent = this.handleEvent.bind(this);
|
||||
this.currentNode = null;
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
this._initMarkup();
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
BoxModelHighlighter.prototype = {
|
||||
BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
|
||||
get zoom() {
|
||||
return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
|
||||
@ -450,61 +609,42 @@ BoxModelHighlighter.prototype = {
|
||||
* Destroy the nodes. Remove listeners.
|
||||
*/
|
||||
destroy: function() {
|
||||
this.hide();
|
||||
|
||||
this.chromeWin.clearTimeout(this.transitionDisabler);
|
||||
this.chromeWin.clearTimeout(this.pageEventsMuter);
|
||||
|
||||
this.nodeInfo = null;
|
||||
XULBasedHighlighter.prototype.destroy.call(this);
|
||||
|
||||
this._highlighterContainer.remove();
|
||||
this._highlighterContainer = null;
|
||||
|
||||
this.nodeInfo = null;
|
||||
this.rect = null;
|
||||
this.win = null;
|
||||
this.browser = null;
|
||||
this.chromeDoc = null;
|
||||
this.chromeWin = null;
|
||||
this.currentNode = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the highlighter on a given node
|
||||
*
|
||||
* @param {DOMNode} node
|
||||
* @param {Object} options
|
||||
* Object used for passing options
|
||||
*/
|
||||
show: function(node, options={}) {
|
||||
this.currentNode = node;
|
||||
|
||||
this._showInfobar();
|
||||
this._detachPageListeners();
|
||||
this._attachPageListeners();
|
||||
_show: function(options={}) {
|
||||
this._update();
|
||||
this._trackMutations();
|
||||
this.emit("ready");
|
||||
},
|
||||
|
||||
/**
|
||||
* Track the current node markup mutations so that the node info bar can be
|
||||
* updated to reflects the node's attributes
|
||||
*/
|
||||
_trackMutations: function() {
|
||||
if (this.currentNode) {
|
||||
if (isNodeValid(this.currentNode)) {
|
||||
let win = this.currentNode.ownerDocument.defaultView;
|
||||
this.currentNodeObserver = new win.MutationObserver(() => {
|
||||
this._update();
|
||||
});
|
||||
this.currentNodeObserver = new win.MutationObserver(this.update);
|
||||
this.currentNodeObserver.observe(this.currentNode, {attributes: true});
|
||||
}
|
||||
},
|
||||
|
||||
_untrackMutations: function() {
|
||||
if (this.currentNode) {
|
||||
if (this.currentNodeObserver) {
|
||||
// The following may fail with a "can't access dead object" exception
|
||||
// when the actor is being destroyed
|
||||
try {
|
||||
this.currentNodeObserver.disconnect();
|
||||
} catch (e) {}
|
||||
this.currentNodeObserver = null;
|
||||
}
|
||||
if (isNodeValid(this.currentNode) && this.currentNodeObserver) {
|
||||
this.currentNodeObserver.disconnect();
|
||||
this.currentNodeObserver = null;
|
||||
}
|
||||
},
|
||||
|
||||
@ -518,29 +658,22 @@ BoxModelHighlighter.prototype = {
|
||||
* the box that the guides should outline. Default is content.
|
||||
*/
|
||||
_update: function(options={}) {
|
||||
if (this.currentNode) {
|
||||
if (this._highlightBoxModel(options)) {
|
||||
this._showInfobar();
|
||||
} else {
|
||||
// Nothing to highlight (0px rectangle like a <script> tag for instance)
|
||||
this.hide();
|
||||
}
|
||||
this.emit("ready");
|
||||
if (this._updateBoxModel(options)) {
|
||||
this._showInfobar();
|
||||
this._showBoxModel();
|
||||
} else {
|
||||
// Nothing to highlight (0px rectangle like a <script> tag for instance)
|
||||
this._hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the highlighter, the outline and the infobar.
|
||||
*/
|
||||
hide: function() {
|
||||
if (this.currentNode) {
|
||||
this._untrackMutations();
|
||||
this.currentNode = null;
|
||||
this._hideBoxModel();
|
||||
this._hideInfobar();
|
||||
this._detachPageListeners();
|
||||
}
|
||||
this.emit("hide");
|
||||
_hide: function() {
|
||||
this._untrackMutations();
|
||||
this._hideBoxModel();
|
||||
this._hideInfobar();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -573,55 +706,40 @@ BoxModelHighlighter.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlight the box model.
|
||||
* Update the box model as per the current node.
|
||||
*
|
||||
* @param {Object} options
|
||||
* Object used for passing options. Valid options are:
|
||||
* - region: "content", "padding", "border" or "margin." This specifies
|
||||
* the region that the guides should outline. Default is content.
|
||||
* @return {boolean}
|
||||
* True if the rectangle was highlighted, false otherwise.
|
||||
* True if the current node has a box model to be highlighted
|
||||
*/
|
||||
_highlightBoxModel: function(options) {
|
||||
let isShown = false;
|
||||
|
||||
_updateBoxModel: function(options) {
|
||||
options.region = options.region || "content";
|
||||
|
||||
this.rect = this.layoutHelpers.getAdjustedQuads(this.currentNode, "margin");
|
||||
|
||||
if (!this.rect) {
|
||||
return null;
|
||||
if (!this.rect || (this.rect.bounds.width <= 0 && this.rect.bounds.height <= 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) {
|
||||
for (let boxType in this._boxModelNodes) {
|
||||
let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
|
||||
this.layoutHelpers.getAdjustedQuads(this.currentNode, boxType);
|
||||
for (let boxType in this._boxModelNodes) {
|
||||
let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
|
||||
this.layoutHelpers.getAdjustedQuads(this.currentNode, boxType);
|
||||
|
||||
let boxNode = this._boxModelNodes[boxType];
|
||||
boxNode.setAttribute("points",
|
||||
p1.x + "," + p1.y + " " +
|
||||
p2.x + "," + p2.y + " " +
|
||||
p3.x + "," + p3.y + " " +
|
||||
p4.x + "," + p4.y);
|
||||
let boxNode = this._boxModelNodes[boxType];
|
||||
boxNode.setAttribute("points",
|
||||
p1.x + "," + p1.y + " " +
|
||||
p2.x + "," + p2.y + " " +
|
||||
p3.x + "," + p3.y + " " +
|
||||
p4.x + "," + p4.y);
|
||||
|
||||
if (boxType === options.region) {
|
||||
this._showGuides(p1, p2, p3, p4);
|
||||
}
|
||||
}
|
||||
|
||||
isShown = true;
|
||||
this._showBoxModel();
|
||||
} else {
|
||||
// Only return false if the element really is invisible.
|
||||
// A height of 0 and a non-0 width corresponds to a visible element that
|
||||
// is below the fold for instance
|
||||
if (this.rect.width > 0 || this.rect.height > 0) {
|
||||
isShown = true;
|
||||
this._hideBoxModel();
|
||||
if (boxType === options.region) {
|
||||
this._showGuides(p1, p2, p3, p4);
|
||||
}
|
||||
}
|
||||
return isShown;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -711,31 +829,40 @@ BoxModelHighlighter.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tag name
|
||||
this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName;
|
||||
let node = this.currentNode;
|
||||
let info = this.nodeInfo;
|
||||
|
||||
// ID
|
||||
this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : "";
|
||||
// Update the tag, id, classes, pseudo-classes and dimensions only if they
|
||||
// changed to avoid triggering paint events
|
||||
|
||||
// Classes
|
||||
let classes = this.nodeInfo.classesBox;
|
||||
let tagName = node.tagName;
|
||||
if (info.tagNameLabel.textContent !== tagName) {
|
||||
info.tagNameLabel.textContent = tagName;
|
||||
}
|
||||
|
||||
classes.textContent = this.currentNode.classList.length ?
|
||||
"." + Array.join(this.currentNode.classList, ".") : "";
|
||||
let id = node.id ? "#" + node.id : "";
|
||||
if (info.idLabel.textContent !== id) {
|
||||
info.idLabel.textContent = id;
|
||||
}
|
||||
|
||||
let classList = node.classList.length ? "." + [...node.classList].join(".") : "";
|
||||
if (info.classesBox.textContent !== classList) {
|
||||
info.classesBox.textContent = classList;
|
||||
}
|
||||
|
||||
// Pseudo-classes
|
||||
let pseudos = PSEUDO_CLASSES.filter(pseudo => {
|
||||
return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo);
|
||||
}, this);
|
||||
return DOMUtils.hasPseudoClassLock(node, pseudo);
|
||||
}, this).join("");
|
||||
if (info.pseudoClassesBox.textContent !== pseudos) {
|
||||
info.pseudoClassesBox.textContent = pseudos;
|
||||
}
|
||||
|
||||
let pseudoBox = this.nodeInfo.pseudoClassesBox;
|
||||
pseudoBox.textContent = pseudos.join("");
|
||||
let rect = node.getBoundingClientRect();
|
||||
let dim = Math.ceil(rect.width) + " x " + Math.ceil(rect.height);
|
||||
if (info.dimensionBox.textContent !== dim) {
|
||||
info.dimensionBox.textContent = dim;
|
||||
}
|
||||
|
||||
// Dimensions
|
||||
let dimensionBox = this.nodeInfo.dimensionBox;
|
||||
let rect = this.currentNode.getBoundingClientRect();
|
||||
dimensionBox.textContent = Math.ceil(rect.width) + " x " +
|
||||
Math.ceil(rect.height);
|
||||
this._moveInfobar();
|
||||
},
|
||||
|
||||
@ -790,44 +917,173 @@ BoxModelHighlighter.prototype = {
|
||||
this.nodeInfo.positioner.setAttribute("position", "top");
|
||||
this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
_attachPageListeners: function() {
|
||||
if (this.currentNode) {
|
||||
let win = this.currentNode.ownerGlobal;
|
||||
/**
|
||||
* The CssTransformHighlighter is the class that draws an outline around a
|
||||
* transformed element and an outline around where it would be if untransformed
|
||||
* as well as arrows connecting the 2 outlines' corners.
|
||||
*/
|
||||
function CssTransformHighlighter(tabActor, inspector) {
|
||||
XULBasedHighlighter.call(this, tabActor, inspector);
|
||||
|
||||
win.addEventListener("scroll", this, false);
|
||||
win.addEventListener("resize", this, false);
|
||||
win.addEventListener("MozAfterPaint", this, false);
|
||||
this.layoutHelpers = new LayoutHelpers(tabActor.window);
|
||||
this._initMarkup();
|
||||
}
|
||||
|
||||
let MARKER_COUNTER = 1;
|
||||
|
||||
CssTransformHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
|
||||
_initMarkup: function() {
|
||||
let stack = this.browser.parentNode;
|
||||
|
||||
this._container = this.chromeDoc.createElement("stack");
|
||||
this._container.className = "highlighter-container";
|
||||
|
||||
this._svgRoot = this._createSVGNode("root", "svg", this._container);
|
||||
this._svgRoot.setAttribute("hidden", "true");
|
||||
|
||||
// Add a marker tag to the svg root for the arrow tip
|
||||
let marker = this.chromeDoc.createElementNS(SVG_NS, "marker");
|
||||
this.markerId = "css-transform-arrow-marker-" + MARKER_COUNTER;
|
||||
MARKER_COUNTER ++;
|
||||
marker.setAttribute("id", this.markerId);
|
||||
marker.setAttribute("markerWidth", "10");
|
||||
marker.setAttribute("markerHeight", "5");
|
||||
marker.setAttribute("orient", "auto");
|
||||
marker.setAttribute("markerUnits", "strokeWidth");
|
||||
marker.setAttribute("refX", "10");
|
||||
marker.setAttribute("refY", "5");
|
||||
marker.setAttribute("viewBox", "0 0 10 10");
|
||||
let path = this.chromeDoc.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
|
||||
path.setAttribute("fill", "#08C");
|
||||
marker.appendChild(path);
|
||||
this._svgRoot.appendChild(marker);
|
||||
|
||||
// Create the 2 polygons (transformed and untransformed)
|
||||
let shapesGroup = this._createSVGNode("container", "g", this._svgRoot);
|
||||
this._shapes = {
|
||||
untransformed: this._createSVGNode("untransformed", "polygon", shapesGroup),
|
||||
transformed: this._createSVGNode("transformed", "polygon", shapesGroup)
|
||||
};
|
||||
|
||||
// Create the arrows
|
||||
for (let nb of ["1", "2", "3", "4"]) {
|
||||
let line = this._createSVGNode("line", "line", shapesGroup);
|
||||
line.setAttribute("marker-end", "url(#" + this.markerId + ")");
|
||||
this._shapes["line" + nb] = line;
|
||||
}
|
||||
|
||||
this._container.appendChild(this._svgRoot);
|
||||
|
||||
// Insert the highlighter right after the browser
|
||||
stack.insertBefore(this._container, stack.childNodes[1]);
|
||||
},
|
||||
|
||||
_detachPageListeners: function() {
|
||||
if (this.currentNode) {
|
||||
let win = this.currentNode.ownerGlobal;
|
||||
_createSVGNode: function(classPostfix, nodeType, parent) {
|
||||
let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
|
||||
node.setAttribute("class", "css-transform-" + classPostfix);
|
||||
|
||||
win.removeEventListener("scroll", this, false);
|
||||
win.removeEventListener("resize", this, false);
|
||||
win.removeEventListener("MozAfterPaint", this, false);
|
||||
parent.appendChild(node);
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy the nodes. Remove listeners.
|
||||
*/
|
||||
destroy: function() {
|
||||
XULBasedHighlighter.prototype.destroy.call(this);
|
||||
|
||||
this._container.remove();
|
||||
this._container = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the highlighter on a given node
|
||||
* @param {DOMNode} node
|
||||
*/
|
||||
_show: function() {
|
||||
if (!this._isTransformed(this.currentNode)) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this._update();
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the supplied node is transformed and not inline
|
||||
*/
|
||||
_isTransformed: function(node) {
|
||||
let style = node.ownerDocument.defaultView.getComputedStyle(node);
|
||||
return style.transform !== "none" && style.display !== "inline";
|
||||
},
|
||||
|
||||
_setPolygonPoints: function(quad, poly) {
|
||||
let points = [];
|
||||
for (let point of ["p1","p2", "p3", "p4"]) {
|
||||
points.push(quad[point].x + "," + quad[point].y);
|
||||
}
|
||||
poly.setAttribute("points", points.join(" "));
|
||||
},
|
||||
|
||||
_setLinePoints: function(p1, p2, line) {
|
||||
line.setAttribute("x1", p1.x);
|
||||
line.setAttribute("y1", p1.y);
|
||||
line.setAttribute("x2", p2.x);
|
||||
line.setAttribute("y2", p2.y);
|
||||
|
||||
let dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
||||
if (dist < ARROW_LINE_MIN_DISTANCE) {
|
||||
line.removeAttribute("marker-end");
|
||||
} else {
|
||||
line.setAttribute("marker-end", "url(#" + this.markerId + ")");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Generic event handler.
|
||||
*
|
||||
* @param nsIDOMEvent aEvent
|
||||
* The DOM event object.
|
||||
* Update the highlighter on the current highlighted node (the one that was
|
||||
* passed as an argument to show(node)).
|
||||
* Should be called whenever node size or attributes change
|
||||
*/
|
||||
handleEvent: function(event) {
|
||||
switch (event.type) {
|
||||
case "resize":
|
||||
case "MozAfterPaint":
|
||||
case "scroll":
|
||||
this._update();
|
||||
break;
|
||||
_update: function() {
|
||||
// Getting the points for the transformed shape
|
||||
let quad = this.layoutHelpers.getAdjustedQuads(this.currentNode, "border");
|
||||
if (!quad || quad.bounds.width <= 0 || quad.bounds.height <= 0) {
|
||||
this._hideShapes();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Getting the points for the untransformed shape
|
||||
let untransformedQuad = this.layoutHelpers.getNodeBounds(this.currentNode);
|
||||
|
||||
this._setPolygonPoints(quad, this._shapes.transformed);
|
||||
this._setPolygonPoints(untransformedQuad, this._shapes.untransformed);
|
||||
for (let nb of ["1", "2", "3", "4"]) {
|
||||
this._setLinePoints(untransformedQuad["p" + nb], quad["p" + nb],
|
||||
this._shapes["line" + nb]);
|
||||
}
|
||||
|
||||
this._showShapes();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the highlighter, the outline and the infobar.
|
||||
*/
|
||||
_hide: function() {
|
||||
this._hideShapes();
|
||||
},
|
||||
|
||||
_hideShapes: function() {
|
||||
this._svgRoot.setAttribute("hidden", "true");
|
||||
},
|
||||
|
||||
_showShapes: function() {
|
||||
this._svgRoot.removeAttribute("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The SimpleOutlineHighlighter is a class that has the same API than the
|
||||
@ -891,6 +1147,40 @@ SimpleOutlineHighlighter.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Can the host support the XUL-based highlighters which require a parent
|
||||
* XUL node to get attached.
|
||||
* @param {TabActor}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function supportXULBasedHighlighter(tabActor) {
|
||||
// Note that <browser>s on Fennec also have a XUL parentNode but the box
|
||||
// model highlighter doesn't display correctly on Fennec (bug 993190)
|
||||
return tabActor.browser &&
|
||||
!!tabActor.browser.parentNode &&
|
||||
Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
|
||||
}
|
||||
|
||||
function isNodeValid(node) {
|
||||
// Is it null or dead?
|
||||
if(!node || Cu.isDeadWrapper(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is it an element node
|
||||
if (node.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is it connected to the document?
|
||||
let doc = node.ownerDocument;
|
||||
if (!doc || !doc.defaultView || !doc.documentElement.contains(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
|
||||
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
|
||||
});
|
||||
|
@ -61,7 +61,11 @@ const events = require("sdk/event/core");
|
||||
const {Unknown} = require("sdk/platform/xpcom");
|
||||
const {Class} = require("sdk/core/heritage");
|
||||
const {PageStyleActor} = require("devtools/server/actors/styles");
|
||||
const {HighlighterActor} = require("devtools/server/actors/highlighter");
|
||||
const {
|
||||
HighlighterActor,
|
||||
CustomHighlighterActor,
|
||||
HIGHLIGHTER_CLASSES
|
||||
} = require("devtools/server/actors/highlighter");
|
||||
const {getLayoutChangesObserver, releaseLayoutChangesObserver} =
|
||||
require("devtools/server/actors/layout");
|
||||
|
||||
@ -2601,6 +2605,19 @@ var InspectorActor = protocol.ActorClass({
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* The most used highlighter actor is the HighlighterActor which can be
|
||||
* conveniently retrieved by this method.
|
||||
* The same instance will always be returned by this method when called
|
||||
* several times.
|
||||
* The highlighter actor returned here is used to highlighter elements's
|
||||
* box-models from the markup-view, layout-view, console, debugger, ... as
|
||||
* well as select elements with the pointer (pick).
|
||||
*
|
||||
* @param {Boolean} autohide Optionally autohide the highlighter after an
|
||||
* element has been picked
|
||||
* @return {HighlighterActor}
|
||||
*/
|
||||
getHighlighter: method(function (autohide) {
|
||||
if (this._highlighterPromise) {
|
||||
return this._highlighterPromise;
|
||||
@ -2611,12 +2628,40 @@ var InspectorActor = protocol.ActorClass({
|
||||
});
|
||||
return this._highlighterPromise;
|
||||
}, {
|
||||
request: { autohide: Arg(0, "boolean") },
|
||||
request: {
|
||||
autohide: Arg(0, "boolean")
|
||||
},
|
||||
response: {
|
||||
highligter: RetVal("highlighter")
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* If consumers need to display several highlighters at the same time or
|
||||
* different types of highlighters, then this method should be used, passing
|
||||
* the type name of the highlighter needed as argument.
|
||||
* A new instance will be created everytime the method is called, so it's up
|
||||
* to the consumer to release it when it is not needed anymore
|
||||
*
|
||||
* @param {String} type The type of highlighter to create
|
||||
* @return {Highlighter} The highlighter actor instance or null if the
|
||||
* typeName passed doesn't match any available highlighter
|
||||
*/
|
||||
getHighlighterByType: method(function (typeName) {
|
||||
if (HIGHLIGHTER_CLASSES[typeName]) {
|
||||
return CustomHighlighterActor(this, typeName);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, {
|
||||
request: {
|
||||
typeName: Arg(0)
|
||||
},
|
||||
response: {
|
||||
highlighter: RetVal("nullable:customhighlighter")
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get the node's image data if any (for canvas and img nodes).
|
||||
* Returns an imageData object with the actual data being a LongStringActor
|
||||
|
@ -104,6 +104,12 @@ RootActor.prototype = {
|
||||
// Whether the server-side highlighter actor exists and can be used to
|
||||
// remotely highlight nodes (see server/actors/highlighter.js)
|
||||
highlightable: true,
|
||||
// Which custom highlighter does the server-side highlighter actor supports?
|
||||
// (see server/actors/highlighter.js)
|
||||
customHighlighters: [
|
||||
"BoxModelHighlighter",
|
||||
"CssTransformHighlighter"
|
||||
],
|
||||
// Whether the inspector actor implements the getImageDataFromURL
|
||||
// method that returns data-uris for image URLs. This is used for image
|
||||
// tooltips for instance
|
||||
|
@ -23,6 +23,9 @@ support-files =
|
||||
[test_framerate_02.html]
|
||||
[test_framerate_03.html]
|
||||
[test_framerate_04.html]
|
||||
[test_highlighter-csstransform_01.html]
|
||||
[test_highlighter-csstransform_02.html]
|
||||
[test_highlighter-csstransform_03.html]
|
||||
[test_inspector-changeattrs.html]
|
||||
[test_inspector-changevalue.html]
|
||||
[test_inspector-hide.html]
|
||||
|
@ -0,0 +1,113 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1014547 - CSS transforms highlighter
|
||||
Test the high level API of the highlighters
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Framerate actor test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script type="application/javascript;version=1.8">
|
||||
|
||||
window.onload = function() {
|
||||
var Cu = Components.utils;
|
||||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
|
||||
|
||||
DebuggerServer.init(() => true);
|
||||
DebuggerServer.addBrowserActors();
|
||||
|
||||
var client = new DebuggerClient(DebuggerServer.connectPipe());
|
||||
client.connect(() => {
|
||||
client.listTabs(response => {
|
||||
var form = response.tabs[response.selected];
|
||||
var front = InspectorFront(client, form);
|
||||
|
||||
Task.spawn(function*() {
|
||||
yield onlyOneInstanceOfMainHighlighter(front);
|
||||
yield manyInstancesOfCustomHighlighters(front);
|
||||
yield showHideMethodsAreAvailable(front);
|
||||
yield unknownHighlighterTypeShouldntBeAccepted(front);
|
||||
yield rootActorTraitsShouldContainKnownTypes(client);
|
||||
}).then(null, ok.bind(null, false)).then(() => {
|
||||
client.close(() => {
|
||||
DebuggerServer.destroy();
|
||||
SimpleTest.finish();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function* onlyOneInstanceOfMainHighlighter(inspectorFront) {
|
||||
info("Check that the inspector always sends back the same main highlighter");
|
||||
|
||||
let h1 = yield inspectorFront.getHighlighter(false);
|
||||
let h2 = yield inspectorFront.getHighlighter(false);
|
||||
is(h1, h2, "The same highlighter front was returned");
|
||||
|
||||
is(h1.typeName, "highlighter", "The right front type was returned");
|
||||
}
|
||||
|
||||
function* manyInstancesOfCustomHighlighters(inspectorFront) {
|
||||
let h1 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
|
||||
let h2 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
|
||||
ok(h1 !== h2, "getHighlighterByType returns new instances every time (1)");
|
||||
|
||||
let h3 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
|
||||
let h4 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
|
||||
ok(h3 !== h4, "getHighlighterByType returns new instances every time (2)");
|
||||
ok(h3 !== h1 && h3 !== h2,
|
||||
"getHighlighterByType returns new instances every time (3)");
|
||||
ok(h4 !== h1 && h4 !== h2,
|
||||
"getHighlighterByType returns new instances every time (4)");
|
||||
|
||||
yield h1.finalize();
|
||||
yield h2.finalize();
|
||||
yield h3.finalize();
|
||||
yield h4.finalize();
|
||||
}
|
||||
|
||||
function* showHideMethodsAreAvailable(inspectorFront) {
|
||||
let h1 = yield inspectorFront.getHighlighterByType("BoxModelHighlighter");
|
||||
let h2 = yield inspectorFront.getHighlighterByType("CssTransformHighlighter");
|
||||
|
||||
ok("show" in h1, "Show method is present on the front API");
|
||||
ok("show" in h2, "Show method is present on the front API");
|
||||
ok("hide" in h1, "Hide method is present on the front API");
|
||||
ok("hide" in h2, "Hide method is present on the front API");
|
||||
|
||||
yield h1.finalize();
|
||||
yield h2.finalize();
|
||||
}
|
||||
|
||||
function* unknownHighlighterTypeShouldntBeAccepted(inspectorFront) {
|
||||
let h = yield inspectorFront.getHighlighterByType("whatever");
|
||||
ok(!h, "No highlighter was returned for the invalid type");
|
||||
}
|
||||
|
||||
function* rootActorTraitsShouldContainKnownTypes(client) {
|
||||
ok(client.traits.customHighlighters.indexOf("BoxModelHighlighter") !== -1,
|
||||
"The root actor's trait contains BoxModelHighlighter as a known type");
|
||||
ok(client.traits.customHighlighters.indexOf("CssTransformHighlighter") !== -1,
|
||||
"The root actor's trait contains CssTransformHighlighter as a known type");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,156 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1014547 - CSS transforms highlighter
|
||||
Test the creation of the SVG highlighter elements in the browser
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Framerate actor test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="transformed" style="border:1px solid red;width:100px;height:100px;transform:skew(13deg);"></div>
|
||||
<div id="untransformed" style="border:1px solid blue;width:100px;height:100px;"></div>
|
||||
<span id="inline" style="transform:rotate(90deg);">this is an inline transformed element</span>
|
||||
<pre id="test">
|
||||
<script type="application/javascript;version=1.8">
|
||||
|
||||
window.onload = function() {
|
||||
var Cu = Components.utils;
|
||||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
|
||||
|
||||
DebuggerServer.init(() => true);
|
||||
DebuggerServer.addBrowserActors();
|
||||
|
||||
var client = new DebuggerClient(DebuggerServer.connectPipe());
|
||||
client.connect(() => {
|
||||
client.listTabs(response => {
|
||||
var form = response.tabs[response.selected];
|
||||
var front = InspectorFront(client, form);
|
||||
|
||||
Task.spawn(function*() {
|
||||
let walkerFront = yield front.getWalker();
|
||||
let highlighterFront = yield front.getHighlighterByType(
|
||||
"CssTransformHighlighter");
|
||||
|
||||
let gBrowser = Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
|
||||
let container =
|
||||
gBrowser.selectedBrowser.parentNode.querySelector(".highlighter-container");
|
||||
ok(container, "The highlighter container was found");
|
||||
|
||||
yield isHiddenByDefault(container);
|
||||
yield has2PolygonsAnd4Lines(container);
|
||||
yield isNotShownForUntransformed(highlighterFront, walkerFront, container);
|
||||
yield isNotShownForInline(highlighterFront, walkerFront, container);
|
||||
yield isVisibleWhenShown(highlighterFront, walkerFront, container);
|
||||
yield linesLinkThePolygons(highlighterFront, walkerFront, container);
|
||||
|
||||
yield highlighterFront.finalize();
|
||||
}).then(null, ok.bind(null, false)).then(() => {
|
||||
client.close(() => {
|
||||
DebuggerServer.destroy();
|
||||
SimpleTest.finish();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function* isHiddenByDefault(container) {
|
||||
let svg = container.querySelector("svg");
|
||||
ok(svg.hasAttribute("hidden"), "The highlighter is hidden by default");
|
||||
}
|
||||
|
||||
function* has2PolygonsAnd4Lines(container) {
|
||||
is(container.querySelectorAll("polygon").length, 2, "Found 2 polygons");
|
||||
is(container.querySelectorAll("line").length, 4, "Found 4 lines");
|
||||
}
|
||||
|
||||
function* isNotShownForUntransformed(highlighterFront, walkerFront, container) {
|
||||
let rawNode = document.getElementById("untransformed");
|
||||
let node = walkerFront.frontForRawNode(rawNode);
|
||||
|
||||
info("Asking to show the highlighter on the untransformed test node");
|
||||
yield highlighterFront.show(node);
|
||||
let svg = container.querySelector("svg");
|
||||
ok(svg.hasAttribute("hidden"), "The highlighter is still hidden");
|
||||
}
|
||||
|
||||
function* isNotShownForInline(highlighterFront, walkerFront, container) {
|
||||
let rawNode = document.getElementById("inline");
|
||||
let node = walkerFront.frontForRawNode(rawNode);
|
||||
|
||||
info("Asking to show the highlighter on the inline test node");
|
||||
yield highlighterFront.show(node);
|
||||
let svg = container.querySelector("svg");
|
||||
ok(svg.hasAttribute("hidden"), "The highlighter is still hidden");
|
||||
}
|
||||
|
||||
function* isVisibleWhenShown(highlighterFront, walkerFront, container) {
|
||||
let rawNode = document.getElementById("transformed");
|
||||
let node = walkerFront.frontForRawNode(rawNode);
|
||||
|
||||
info("Asking to show the highlighter on the test node");
|
||||
yield highlighterFront.show(node);
|
||||
let svg = container.querySelector("svg");
|
||||
ok(!svg.hasAttribute("hidden"), "The highlighter is visible");
|
||||
|
||||
info("Hiding the highlighter");
|
||||
yield highlighterFront.hide();
|
||||
ok(svg.hasAttribute("hidden"), "The highlighter is hidden");
|
||||
}
|
||||
|
||||
function* linesLinkThePolygons(highlighterFront, walkerFront, container) {
|
||||
let rawNode = document.getElementById("transformed");
|
||||
let node = walkerFront.frontForRawNode(rawNode);
|
||||
|
||||
info("Showing the highlighter on the transformed node");
|
||||
yield highlighterFront.show(node);
|
||||
|
||||
info("Checking that the 4 lines do link the 2 shape's corners");
|
||||
let lines = [...container.querySelectorAll("line")];
|
||||
|
||||
let polygon1 = container.querySelector(".css-transform-untransformed");
|
||||
let points1 = polygon1.getAttribute("points").split(" ");
|
||||
|
||||
let polygon2 = container.querySelector(".css-transform-transformed");
|
||||
let points2 = polygon2.getAttribute("points").split(" ");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
info("Checking line nb " + i);
|
||||
let line = lines[i];
|
||||
|
||||
let p1 = points1[i].split(",");
|
||||
let x1 = line.getAttribute("x1");
|
||||
let y1 = line.getAttribute("y1");
|
||||
is(p1[0], x1, "line " + i + "'s first point matches the untransformed x coordinate");
|
||||
is(p1[1], y1, "line " + i + "'s first point matches the untransformed y coordinate");
|
||||
|
||||
let p2 = points2[i].split(",");
|
||||
let x2 = line.getAttribute("x2");
|
||||
let y2 = line.getAttribute("y2");
|
||||
is(p2[0], x2, "line " + i + "'s first point matches the transformed x coordinate");
|
||||
is(p2[1], y2, "line " + i + "'s first point matches the transformed y coordinate");
|
||||
}
|
||||
|
||||
yield highlighterFront.hide();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,119 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1014547 - CSS transforms highlighter
|
||||
Test that the highlighter elements created have the right size and coordinates.
|
||||
|
||||
Note that instead of hard-coding values here, the assertions are made by
|
||||
comparing with the result of LayoutHelpers.getAdjustedQuads.
|
||||
|
||||
There's a separate test for checking that getAdjustedQuads actually returns
|
||||
sensible values
|
||||
(browser/devtools/shared/test/browser_layoutHelpers-getBoxQuads.js),
|
||||
so the present test doesn't care about that, it just verifies that the css
|
||||
transform highlighter applies those values correctly to the SVG elements
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Framerate actor test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<style type="text/css">
|
||||
#test-node {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
|
||||
transform: rotate(90deg) skew(13deg) scale(.8) translateX(50px);
|
||||
transform-origin: 50%;
|
||||
|
||||
background: linear-gradient(green, yellow);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test-node"></div>
|
||||
<pre id="test">
|
||||
<script type="application/javascript;version=1.8">
|
||||
|
||||
window.onload = function() {
|
||||
var Cu = Components.utils;
|
||||
var Cc = Components.classes;
|
||||
var Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var {InspectorFront} = devtools.require("devtools/server/actors/inspector");
|
||||
|
||||
DebuggerServer.init(() => true);
|
||||
DebuggerServer.addBrowserActors();
|
||||
|
||||
var client = new DebuggerClient(DebuggerServer.connectPipe());
|
||||
client.connect(() => {
|
||||
client.listTabs(response => {
|
||||
var form = response.tabs[response.selected];
|
||||
var front = InspectorFront(client, form);
|
||||
|
||||
Task.spawn(function*() {
|
||||
let walker = yield front.getWalker();
|
||||
let highlighter = yield front.getHighlighterByType(
|
||||
"CssTransformHighlighter");
|
||||
|
||||
let browser = Services.wm.getMostRecentWindow("navigator:browser")
|
||||
.gBrowser.selectedBrowser;
|
||||
|
||||
let container = browser.parentNode.querySelector(".highlighter-container");
|
||||
|
||||
let node = document.querySelector("#test-node");
|
||||
let helper = new LayoutHelpers(browser.docShell
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow));
|
||||
|
||||
info("Displaying the transform highlighter on test node " +
|
||||
node.tagName);
|
||||
yield highlighter.show(walker.frontForRawNode(node));
|
||||
|
||||
let expected = helper.getAdjustedQuads(node, "border");
|
||||
let polygon = container.querySelector(".css-transform-transformed");
|
||||
let polygonPoints = polygon.getAttribute("points").split(" ").map(p => {
|
||||
return {
|
||||
x: +p.substring(0, p.indexOf(",")),
|
||||
y: +p.substring(p.indexOf(",")+1)
|
||||
};
|
||||
});
|
||||
|
||||
for (let i = 1; i < 5; i ++) {
|
||||
is(polygonPoints[i - 1].x, expected["p" + i].x,
|
||||
"p" + i + " x coordinate is correct");
|
||||
is(polygonPoints[i - 1].y, expected["p" + i].y,
|
||||
"p" + i + " y coordinate is correct");
|
||||
}
|
||||
|
||||
info("Hiding the transform highlighter");
|
||||
yield highlighter.hide();
|
||||
|
||||
yield highlighter.finalize();
|
||||
}).then(null, ok.bind(null, false)).then(() => {
|
||||
client.close(() => {
|
||||
DebuggerServer.destroy();
|
||||
SimpleTest.finish();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user