Bug 1139925 - Make the BoxModelHighlighter highlight all quads and draw guides around the outer-most rect

LayoutHelpers.getAdjustedBoxQuads now returns all quads that el.getBoxQuads returns.
The BoxModelHighlighter calculates an outer rect based on these to draw the guides.
And if the element has more than 1 quad (inline element that spans line breaks), then all quads are
highlighted.
Also all related tests were modified and a couple of new tests were added.
This commit is contained in:
Patrick Brosset 2015-03-13 15:54:10 +01:00
parent 60a2beb02f
commit 60c76b4463
15 changed files with 474 additions and 197 deletions

View File

@ -12,6 +12,7 @@ support-files =
doc_inspector_highlighter-geometry_02.html
doc_inspector_highlighter_csstransform.html
doc_inspector_highlighter_dom.html
doc_inspector_highlighter_inline.html
doc_inspector_highlighter.html
doc_inspector_highlighter_rect.html
doc_inspector_highlighter_rect_iframe.html
@ -51,6 +52,7 @@ skip-if = e10s # GCLI isn't e10s compatible. See bug 1128988.
[browser_inspector_highlighter-hover_02.js]
[browser_inspector_highlighter-hover_03.js]
[browser_inspector_highlighter-iframes.js]
[browser_inspector_highlighter-inline.js]
[browser_inspector_highlighter-keybinding_01.js]
[browser_inspector_highlighter-keybinding_02.js]
[browser_inspector_highlighter-keybinding_03.js]

View File

@ -21,7 +21,7 @@ add_task(function*() {
let highlightedNode = yield getHighlitNode(toolbox);
is(highlightedNode, getNode("#simple-div"),
"The highlighter's outline corresponds to the simple div");
yield isNodeCorrectlyHighlighted(getNode("#simple-div"), toolbox,
yield isNodeCorrectlyHighlighted("#simple-div", toolbox,
"non-zoomed");
info("Selecting the rotated DIV");
@ -29,7 +29,7 @@ add_task(function*() {
isVisible = yield isHighlighting(toolbox);
ok(isVisible, "The highlighter is shown");
yield isNodeCorrectlyHighlighted(getNode("#rotated-div"), toolbox,
yield isNodeCorrectlyHighlighted("#rotated-div", toolbox,
"rotated");
info("Selecting the zero width height DIV");
@ -37,6 +37,6 @@ add_task(function*() {
isVisible = yield isHighlighting(toolbox);
ok(isVisible, "The highlighter is shown");
yield isNodeCorrectlyHighlighted(getNode("#widthHeightZero-div"), toolbox,
yield isNodeCorrectlyHighlighted("#widthHeightZero-div", toolbox,
"zero width height");
});

View File

@ -45,7 +45,7 @@ add_task(function* () {
yield moveMouseOver(iframeNode, 1, 1);
info("Performing checks");
yield isNodeCorrectlyHighlighted(iframeNode, toolbox);
yield isNodeCorrectlyHighlighted("iframe", toolbox);
info("Scrolling the document");
iframeNode.style.marginBottom = content.innerHeight + "px";
@ -58,7 +58,7 @@ add_task(function* () {
let highlightedNode = yield getHighlitNode(toolbox);
is(highlightedNode, iframeBodyNode, "highlighter shows the right node");
yield isNodeCorrectlyHighlighted(iframeBodyNode, toolbox);
yield isNodeCorrectlyHighlighted("iframe || body", toolbox);
info("Waiting for the element picker to deactivate.");
yield inspector.toolbox.highlighterUtils.stopPicker();

View File

@ -26,14 +26,15 @@ add_task(function*() {
let highlighter = yield front.getHighlighterByType("CssTransformHighlighter");
let node = getNode("#test-node");
let nodeFront = yield getNodeFront("#test-node", inspector);
info("Displaying the transform highlighter on test node");
yield highlighter.show(nodeFront);
let {data} = yield executeInContent("Test:GetAllAdjustedQuads", null, {node});
let expected = data.border;
let {data} = yield executeInContent("Test:GetAllAdjustedQuads", {
selector: "#test-node"
});
let [expected] = data.border;
let points = yield getHighlighterNodeAttribute(highlighter,
"css-transform-transformed", "points");

View File

@ -0,0 +1,72 @@
/* 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";
// Test that highlighting various inline boxes displays the right number of
// polygons in the page.
const TEST_URL = TEST_URL_ROOT + "doc_inspector_highlighter_inline.html";
const TEST_DATA = [
"body",
"h1",
"h2",
"h2 em",
"p",
"p span",
// The following test case used to fail. See bug 1139925.
"[dir=rtl] > span"
];
add_task(function*() {
info("Loading the test document and opening the inspector");
let {toolbox, inspector} = yield openInspectorForURL(TEST_URL);
for (let selector of TEST_DATA) {
info("Selecting and highlighting node " + selector);
yield selectAndHighlightNode(selector, inspector);
info("Get all quads for this node");
let {data} = yield executeInContent("Test:GetAllAdjustedQuads", {selector});
info("Iterate over the box-model regions and verify that the highlighter is correct");
for (let region of ["margin", "border", "padding", "content"]) {
let {points} = yield getHighlighterRegionPath(region, toolbox.highlighter);
is(points.length, data[region].length,
"The highlighter's " + region + " path defines the correct number of boxes");
}
info("Verify that the guides define a rectangle that contains all content boxes");
let expectedContentRect = {
p1: {x: Infinity, y: Infinity},
p2: {x: -Infinity, y: Infinity},
p3: {x: -Infinity, y: -Infinity},
p4: {x: Infinity, y: -Infinity}
};
for (let {p1, p2, p3, p4} of data.content) {
expectedContentRect.p1.x = Math.min(expectedContentRect.p1.x, p1.x);
expectedContentRect.p1.y = Math.min(expectedContentRect.p1.y, p1.y);
expectedContentRect.p2.x = Math.max(expectedContentRect.p2.x, p2.x);
expectedContentRect.p2.y = Math.min(expectedContentRect.p2.y, p2.y);
expectedContentRect.p3.x = Math.max(expectedContentRect.p3.x, p3.x);
expectedContentRect.p3.y = Math.max(expectedContentRect.p3.y, p3.y);
expectedContentRect.p4.x = Math.min(expectedContentRect.p4.x, p4.x);
expectedContentRect.p4.y = Math.max(expectedContentRect.p4.y, p4.y);
}
let contentRect = yield getGuidesRectangle(toolbox);
for (let point of ["p1", "p2", "p3", "p4"]) {
is((contentRect[point].x),
(expectedContentRect[point].x),
"x coordinate of point " + point +
" of the content rectangle defined by the outer guides is correct");
is((contentRect[point].y),
(expectedContentRect[point].y),
"y coordinate of point " + point +
" of the content rectangle defined by the outer guides is correct");
}
}
});

View File

@ -41,9 +41,8 @@ const TEST_DATA = [
options: {},
checkHighlighter: function*(toolbox) {
for (let region of ["margin", "border", "padding", "content"]) {
let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-" + region, "points");
ok(points, "Region " + region + " has set coordinates");
let {d} = yield getHighlighterRegionPath(region, toolbox.highlighter);
ok(d, "Region " + region + " has set coordinates");
}
}
},
@ -71,42 +70,34 @@ const TEST_DATA = [
desc: "One region only can be shown (1)",
options: {showOnly: "content"},
checkHighlighter: function*(toolbox) {
let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-margin", "points");
ok(!points, "margin region is hidden");
let {d} = yield getHighlighterRegionPath("margin", toolbox.highlighter);
ok(!d, "margin region is hidden");
points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-border", "points");
ok(!points, "border region is hidden");
({d}) = yield getHighlighterRegionPath("border", toolbox.highlighter);
ok(!d, "border region is hidden");
points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-padding", "points");
ok(!points, "padding region is hidden");
({d}) = yield getHighlighterRegionPath("padding", toolbox.highlighter);
ok(!d, "padding region is hidden");
points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-content", "points");
ok(points, "content region is shown");
({d}) = yield getHighlighterRegionPath("content", toolbox.highlighter);
ok(d, "content region is shown");
}
},
{
desc: "One region only can be shown (2)",
options: {showOnly: "margin"},
checkHighlighter: function*(toolbox) {
let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-margin", "points");
ok(points, "margin region is shown");
let {d} = yield getHighlighterRegionPath("margin", toolbox.highlighter);
ok(d, "margin region is shown");
points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-border", "points");
ok(!points, "border region is hidden");
({d}) = yield getHighlighterRegionPath("border", toolbox.highlighter);
ok(!d, "border region is hidden");
points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-padding", "points");
ok(!points, "padding region is hidden");
({d}) = yield getHighlighterRegionPath("padding", toolbox.highlighter);
ok(!d, "padding region is hidden");
points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-content", "points");
ok(!points, "content region is hidden");
({d}) = yield getHighlighterRegionPath("content", toolbox.highlighter);
ok(!d, "content region is hidden");
}
},
{
@ -122,9 +113,8 @@ const TEST_DATA = [
let leftX1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-guide-left", "x1");
let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-padding", "points");
points = points.split(" ").map(xy => xy.split(","));
let {points} = yield getHighlighterRegionPath("padding", toolbox.highlighter);
points = points[0];
is(Math.ceil(topY1), points[0][1], "Top guide's y1 is correct");
is(Math.floor(rightX1), points[1][0], "Right guide's x1 is correct");
@ -145,10 +135,9 @@ const TEST_DATA = [
let leftX1 = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-guide-left", "x1");
let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-margin", "points");
let {points} = yield getHighlighterRegionPath("margin", toolbox.highlighter);
points = points[0];
points = points.split(" ").map(xy => xy.split(","));
is(Math.ceil(topY1), points[0][1], "Top guide's y1 is correct");
is(Math.floor(rightX1), points[1][0], "Right guide's x1 is correct");
is(Math.floor(bottomY1), points[2][1], "Bottom guide's y1 is correct");

View File

@ -41,7 +41,7 @@ add_task(function*() {
isVisible = yield isHighlighting(toolbox);
ok(isVisible, "The highlighter is still visible at zoom level " + level);
yield isNodeCorrectlyHighlighted(getNode("div"), toolbox);
yield isNodeCorrectlyHighlighted("div", toolbox);
info("Check that the highlighter root wrapper node was scaled down");

View File

@ -214,12 +214,14 @@ addMessageListener("Test:ElementFromPoint", function(msg) {
/**
* Get all box-model regions' adjusted boxquads for the given element
* @param {Object} msg The msg.objects part should be the element
* @param {Object} msg The msg.data part should contain the node selector.
* @return {Object} An object with each property being a box-model region, each
* of them being an object with the p1/p2/p3/p4 properties
* of them being an array of objects with the p1/p2/p3/p4 properties.
*/
addMessageListener("Test:GetAllAdjustedQuads", function(msg) {
let {node} = msg.objects;
let {selector} = msg.data;
let node = superQuerySelector(selector);
let regions = {};
let helper = new LayoutHelpers(content);
@ -250,7 +252,7 @@ addMessageListener("Test:SynthesizeMouse", function(msg) {
let {node} = msg.objects;
if (!node && selector) {
node = content.document.querySelector(selector);
node = superQuerySelector(selector);
}
if (center) {
@ -293,4 +295,30 @@ addMessageListener("Test:HasPseudoClassLock", function(msg) {
sendAsyncMessage("Test:HasPseudoClassLock", DOMUtils.hasPseudoClassLock(node, pseudo));
});
/**
* Like document.querySelector but can go into iframes too.
* ".container iframe || .sub-container div" will first try to find the node
* matched by ".container iframe" in the root document, then try to get the
* content document inside it, and then try to match ".sub-container div" inside
* this document.
* Any selector coming before the || separator *MUST* match a frame node.
* @param {String} superSelector.
* @return {DOMNode} The node, or null if not found.
*/
function superQuerySelector(superSelector, root=content.document) {
let frameIndex = superSelector.indexOf("||");
if (frameIndex === -1) {
return root.querySelector(superSelector);
} else {
let rootSelector = superSelector.substring(0, frameIndex).trim();
let childSelector = superSelector.substring(frameIndex+2).trim();
root = root.querySelector(rootSelector);
if (!root || !root.contentWindow) {
return null;
}
return superQuerySelector(childSelector, root.contentWindow.document);
}
}
let dumpn = msg => dump(msg + "\n");

View File

@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
div {
position:absolute;

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
html {
height: 100%;
background: #eee;
}
body {
margin: 0 auto;
padding: 1em;
box-sizing: border-box;
width: 500px;
height: 100%;
background: white;
font-family: Arial;
font-size: 15px;
line-height: 40px;
}
p span {
padding: 5px 0;
margin: 0 5px;
border: 5px solid #eee;
}
</style>
</head>
<body>
<h1>Lorem Ipsum</h1>
<h2>Lorem ipsum <em>dolor sit amet</em></h2>
<p><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed varius, nisl eget semper maximus, dui tellus tempor leo, at pharetra eros tortor sed odio. Nullam sagittis ex nec mi sagittis pulvinar. Pellentesque dapibus feugiat fermentum. Curabitur lacinia quis enim et tristique. Aliquam in semper massa. In ac vulputate nunc, at rutrum neque. Fusce condimentum, tellus quis placerat imperdiet, dolor tortor mattis erat, nec luctus magna diam pharetra mauris.</span></p>
<div dir="rtl">
<span><span></span>some ltr text in an rtl container</span>
</div>
</body>
</html>

View File

@ -306,14 +306,22 @@ let getBoxModelStatus = Task.async(function*(toolbox) {
return ret;
});
let getGuideStatus = Task.async(function*(location, toolbox) {
/**
* Get data about one of the toolbox box-model highlighter's guides.
* @param {String} location One of top, right, bottom, left.
* @param {Toolbox} toolbox The toolbox instance, used to retrieve the highlighter.
* @return {Object} The returned object has the following form:
* - visible {Boolean} Whether that guide is visible.
* - x1/y1/x2/y2 {String} The <line>'s coordinates.
*/
let getGuideStatus = Task.async(function*(location, {highlighter}) {
let id = "box-model-guide-" + location;
let hidden = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "hidden");
let x1 = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "x1");
let y1 = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "y1");
let x2 = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "x2");
let y2 = yield getHighlighterNodeAttribute(toolbox.highlighter, id, "y2");
let hidden = yield getHighlighterNodeAttribute(highlighter, id, "hidden");
let x1 = yield getHighlighterNodeAttribute(highlighter, id, "x1");
let y1 = yield getHighlighterNodeAttribute(highlighter, id, "y1");
let x2 = yield getHighlighterNodeAttribute(highlighter, id, "x2");
let y2 = yield getHighlighterNodeAttribute(highlighter, id, "y2");
return {
visible: !hidden,
@ -325,36 +333,68 @@ let getGuideStatus = Task.async(function*(location, toolbox) {
});
/**
* Get the coordinate (points attribute) from one of the polygon elements in the
* box model highlighter.
* Get the coordinates of the rectangle that is defined by the 4 guides displayed
* in the toolbox box-model highlighter.
* @param {Toolbox} toolbox The toolbox instance, used to retrieve the highlighter.
* @return {Object} Null if at least one guide is hidden. Otherwise an object
* with p1, p2, p3, p4 properties being {x, y} objects.
*/
let getGuidesRectangle = Task.async(function*(toolbox) {
let tGuide = yield getGuideStatus("top", toolbox);
let rGuide = yield getGuideStatus("right", toolbox);
let bGuide = yield getGuideStatus("bottom", toolbox);
let lGuide = yield getGuideStatus("left", toolbox);
if (!tGuide.visible || !rGuide.visible || !bGuide.visible || !lGuide.visible) {
return null;
}
return {
p1: {x: lGuide.x1, y: tGuide.y1},
p2: {x: rGuide.x1, y: tGuide. y1},
p3: {x: rGuide.x1, y: bGuide.y1},
p4: {x: lGuide.x1, y: bGuide.y1}
};
});
/**
* Get the coordinate (points defined by the d attribute) from one of the path
* elements in the box model highlighter.
*/
let getPointsForRegion = Task.async(function*(region, toolbox) {
let points = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-" + region, "points");
points = points.split(/[, ]/);
let d = yield getHighlighterNodeAttribute(toolbox.highlighter,
"box-model-" + region, "d");
let polygons = d.match(/M[^M]+/g);
if (!polygons) {
return null;
}
let points = polygons[0].trim().split(" ").map(i => {
return i.replace(/M|L/, "").split(",")
});
return {
p1: {
x: parseFloat(points[0]),
y: parseFloat(points[1])
x: parseFloat(points[0][0]),
y: parseFloat(points[0][1])
},
p2: {
x: parseFloat(points[2]),
y: parseFloat(points[3])
x: parseFloat(points[1][0]),
y: parseFloat(points[1][1])
},
p3: {
x: parseFloat(points[4]),
y: parseFloat(points[5])
x: parseFloat(points[2][0]),
y: parseFloat(points[2][1])
},
p4: {
x: parseFloat(points[6]),
y: parseFloat(points[7])
x: parseFloat(points[3][0]),
y: parseFloat(points[3][1])
}
};
});
/**
* Is a given region polygon element of the box-model highlighter currently
* Is a given region path element of the box-model highlighter currently
* hidden?
*/
let isRegionHidden = Task.async(function*(region, toolbox) {
@ -387,27 +427,24 @@ let getHighlitNode = Task.async(function*(toolbox) {
/**
* Assert that the box-model highlighter's current position corresponds to the
* given node boxquads.
* @param {DOMNode|CPOW} node The node to get the boxQuads from
* @param {String} selector The selector for the node to get the boxQuads from
* @param {String} prefix An optional prefix for logging information to the
* console.
*/
let isNodeCorrectlyHighlighted = Task.async(function*(node, toolbox, prefix="") {
prefix += (prefix ? " " : "") + node.nodeName;
prefix += (node.id ? "#" + node.id : "");
prefix += (node.classList.length ? "." + [...node.classList].join(".") : "");
prefix += " ";
let isNodeCorrectlyHighlighted = Task.async(function*(selector, toolbox, prefix="") {
let boxModel = yield getBoxModelStatus(toolbox);
let {data: regions} = yield executeInContent("Test:GetAllAdjustedQuads", null,
{node});
let {data: regions} = yield executeInContent("Test:GetAllAdjustedQuads",
{selector});
for (let boxType of ["content", "padding", "border", "margin"]) {
let quads = regions[boxType];
let [quad] = regions[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");
is(boxModel[boxType].points[point].x, quad[point].x,
"Node " + selector + " " + boxType + " point " + point +
" x coordinate is correct");
is(boxModel[boxType].points[point].y, quad[point].y,
"Node " + selector + " " + boxType + " point " + point +
" y coordinate is correct");
}
}
});
@ -552,6 +589,37 @@ let getHighlighterNodeAttribute = Task.async(function*(highlighter, nodeID, name
return value;
});
/**
* Get the "d" attribute value for one of the box-model highlighter's region
* <path> elements, and parse it to a list of points.
* @param {String} region The box model region name.
* @param {Front} highlighter The front of the highlighter.
* @return {Object} The object returned has the following form:
* - d {String} the d attribute value
* - points {Array} an array of all the polygons defined by the path. Each box
* is itself an Array of points, themselves being [x,y] coordinates arrays.
*/
let getHighlighterRegionPath = Task.async(function*(region, highlighter) {
let d = yield getHighlighterNodeAttribute(highlighter, "box-model-" + region, "d");
if (!d) {
return {d: null};
}
let polygons = d.match(/M[^M]+/g);
if (!polygons) {
return {d};
}
let points = [];
for (let polygon of polygons) {
points.push(polygon.trim().split(" ").map(i => {
return i.replace(/M|L/, "").split(",")
}));
}
return {d, points};
});
/**
* Get the textContent value of one of the highlighter's node.
* @param {Front} highlighter The front of the highlighter.

View File

@ -59,4 +59,7 @@
<div id="sub-scrolled-node">
<div id="inner-scrolled-node"></div>
</div>
</div>
</div>
<span id="inline">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus porttitor luctus sem id scelerisque. Cras quis velit sed risus euismod lacinia. Donec viverra enim eu ligula efficitur, quis vulputate metus cursus. Duis sed interdum risus. Ut blandit velit vitae faucibus efficitur. Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br/ >
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed vitae dolor metus. Aliquam sed velit sit amet libero vestibulum aliquam vel a lorem. Integer eget ex eget justo auctor ullamcorper.<br/ >
Praesent tristique maximus lacus, nec ultricies neque ultrices non. Phasellus vel lobortis justo. </span>

View File

@ -21,13 +21,14 @@ function test() {
info("Running tests");
returnsTheRightDataStructure(doc, helper);
returnsNullForMissingNode(doc, helper);
returnsNullForHiddenNodes(doc, helper);
isEmptyForMissingNode(doc, helper);
isEmptyForHiddenNodes(doc, helper);
defaultsToBorderBoxIfNoneProvided(doc, helper);
returnsLikeGetBoxQuadsInSimpleCase(doc, helper);
takesIframesOffsetsIntoAccount(doc, helper);
takesScrollingIntoAccount(doc, helper);
takesZoomIntoAccount(doc, helper);
returnsMultipleItemsForWrappingInlineElements(doc, helper);
gBrowser.removeCurrentTab();
finish();
@ -38,7 +39,7 @@ 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");
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");
@ -58,24 +59,24 @@ function returnsTheRightDataStructure(doc, helper) {
}
}
function returnsNullForMissingNode(doc, helper) {
function isEmptyForMissingNode(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);
is(helper.getAdjustedQuads(input).length, 0, "A 0-length array is returned" +
"for input " + input);
}
}
function returnsNullForHiddenNodes(doc, helper) {
function isEmptyForHiddenNodes(doc, helper) {
info("Checks that null is returned for nodes that aren't rendered");
let style = doc.querySelector("#styles");
ok(helper.getAdjustedQuads(style) === null,
is(helper.getAdjustedQuads(style).length, 0,
"null is returned for a <style> node");
let hidden = doc.querySelector("#hidden-node");
ok(helper.getAdjustedQuads(hidden) === null,
is(helper.getAdjustedQuads(hidden).length, 0,
"null is returned for a hidden node");
}
@ -83,8 +84,8 @@ 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);
let [withBoxType] = helper.getAdjustedQuads(node, "border");
let [withoutBoxType] = helper.getAdjustedQuads(node);
for (let boundProp of
["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
@ -111,7 +112,7 @@ function returnsLikeGetBoxQuadsInSimpleCase(doc, helper) {
let expected = node.getBoxQuads({
box: region
})[0];
let actual = helper.getAdjustedQuads(node, region);
let [actual] = helper.getAdjustedQuads(node, region);
for (let boundProp of
["bottom", "top", "right", "left", "width", "height", "x", "y"]) {
@ -138,7 +139,7 @@ function takesIframesOffsetsIntoAccount(doc, helper) {
let subIframe = rootIframe.contentDocument.querySelector("iframe");
let innerNode = subIframe.contentDocument.querySelector("#inner-node");
let quad = helper.getAdjustedQuads(innerNode, "content");
let [quad] = helper.getAdjustedQuads(innerNode, "content");
//rootIframe margin + subIframe margin + node margin + node border + node padding
let p1x = 10 + 10 + 10 + 10 + 10;
@ -163,7 +164,7 @@ function takesScrollingIntoAccount(doc, helper) {
subScrolledNode.scrollTop = 200;
let innerNode = doc.querySelector("#inner-scrolled-node");
let quad = helper.getAdjustedQuads(innerNode, "content");
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");
@ -171,7 +172,7 @@ function takesScrollingIntoAccount(doc, helper) {
scrolledNode.scrollTop = 0;
subScrolledNode.scrollTop = 0;
quad = helper.getAdjustedQuads(innerNode, "content");
[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");
}
@ -184,11 +185,11 @@ function takesZoomIntoAccount(doc, helper) {
// bigger quad and zooming out produces a smaller quad
let node = doc.querySelector("#simple-node-with-margin-padding-border");
let defaultQuad = helper.getAdjustedQuads(node);
let [defaultQuad] = helper.getAdjustedQuads(node);
info("Zoom in");
window.FullZoom.enlarge();
let zoomedInQuad = helper.getAdjustedQuads(node);
let [zoomedInQuad] = helper.getAdjustedQuads(node);
ok(zoomedInQuad.bounds.width > defaultQuad.bounds.width,
"The zoomed in quad is bigger than the default one");
@ -198,7 +199,7 @@ function takesZoomIntoAccount(doc, helper) {
info("Zoom out");
window.FullZoom.reset();
window.FullZoom.reduce();
let zoomedOutQuad = helper.getAdjustedQuads(node);
let [zoomedOutQuad] = helper.getAdjustedQuads(node);
ok(zoomedOutQuad.bounds.width < defaultQuad.bounds.width,
"The zoomed out quad is smaller than the default one");
@ -207,3 +208,15 @@ function takesZoomIntoAccount(doc, helper) {
window.FullZoom.reset();
}
function returnsMultipleItemsForWrappingInlineElements(doc, helper) {
info("Checks that several quads are returned for inline elements that span line-breaks");
let node = doc.querySelector("#inline");
let quads = helper.getAdjustedQuads(node, "content");
// At least 3 because of the 2 <br />, maybe more depending on the window size.
ok(quads.length >= 3, "Multiple quads were returned");
is(quads.length, node.getBoxQuads().length,
"The same number of boxes as getBoxQuads was returned");
}

View File

@ -27,67 +27,71 @@ 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
* @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 {Array} An array of objects that have the same structure as quads
* returned by getBoxQuads. An empty array if the node has no quads or is
* invalid.
*/
getAdjustedQuads: function(node, region) {
if (!node || !node.getBoxQuads) {
return null;
return [];
}
let [quads] = node.getBoxQuads({
let quads = node.getBoxQuads({
box: region
});
if (!quads) {
return null;
if (!quads.length) {
return [];
}
let [xOffset, yOffset] = this.getFrameOffsets(node);
let scale = LayoutHelpers.getCurrentZoom(node);
return {
p1: {
w: quads.p1.w * scale,
x: quads.p1.x * scale + xOffset,
y: quads.p1.y * scale + yOffset,
z: quads.p1.z * scale
},
p2: {
w: quads.p2.w * scale,
x: quads.p2.x * scale + xOffset,
y: quads.p2.y * scale + yOffset,
z: quads.p2.z * scale
},
p3: {
w: quads.p3.w * scale,
x: quads.p3.x * scale + xOffset,
y: quads.p3.y * scale + yOffset,
z: quads.p3.z * scale
},
p4: {
w: quads.p4.w * scale,
x: quads.p4.x * scale + xOffset,
y: quads.p4.y * scale + yOffset,
z: quads.p4.z * scale
},
bounds: {
bottom: quads.bounds.bottom * scale + yOffset,
height: quads.bounds.height * scale,
left: quads.bounds.left * scale + xOffset,
right: quads.bounds.right * scale + xOffset,
top: quads.bounds.top * scale + yOffset,
width: quads.bounds.width * scale,
x: quads.bounds.x * scale + xOffset,
y: quads.bounds.y * scale + yOffset
}
};
let adjustedQuads = [];
for (let quad of quads) {
adjustedQuads.push({
p1: {
w: quad.p1.w * scale,
x: quad.p1.x * scale + xOffset,
y: quad.p1.y * scale + yOffset,
z: quad.p1.z * scale
},
p2: {
w: quad.p2.w * scale,
x: quad.p2.x * scale + xOffset,
y: quad.p2.y * scale + yOffset,
z: quad.p2.z * scale
},
p3: {
w: quad.p3.w * scale,
x: quad.p3.x * scale + xOffset,
y: quad.p3.y * scale + yOffset,
z: quad.p3.z * scale
},
p4: {
w: quad.p4.w * scale,
x: quad.p4.x * scale + xOffset,
y: quad.p4.y * scale + yOffset,
z: quad.p4.z * scale
},
bounds: {
bottom: quad.bounds.bottom * scale + yOffset,
height: quad.bounds.height * scale,
left: quad.bounds.left * scale + xOffset,
right: quad.bounds.right * scale + xOffset,
top: quad.bounds.top * scale + yOffset,
width: quad.bounds.width * scale,
x: quad.bounds.x * scale + xOffset,
y: quad.bounds.y * scale + yOffset
}
});
}
return adjustedQuads;
},
/**

View File

@ -829,9 +829,10 @@ AutoRefreshHighlighter.prototype = {
};
/**
* The BoxModelHighlighter is the class that actually draws the the box model
* regions on top of a node.
* It is used by the HighlighterActor.
* The BoxModelHighlighter draws the box model regions on top of a node.
* If the node is a block box, then each region will be displayed as 1 polygon.
* If the node is an inline box though, each region may be represented by 1 or
* more polygons, depending on how many line boxes the inline element has.
*
* Usage example:
*
@ -858,10 +859,10 @@ AutoRefreshHighlighter.prototype = {
* <div class="box-model-root">
* <svg class="box-model-elements" hidden="true">
* <g class="box-model-regions">
* <polygon class="box-model-margin" points="..." />
* <polygon class="box-model-border" points="..." />
* <polygon class="box-model-padding" points="..." />
* <polygon class="box-model-content" points="..." />
* <path class="box-model-margin" points="..." />
* <path class="box-model-border" points="..." />
* <path class="box-model-padding" points="..." />
* <path class="box-model-content" points="..." />
* </g>
* <line class="box-model-guide-top" x1="..." y1="..." x2="..." y2="..." />
* <line class="box-model-guide-right" x1="..." y1="..." x2="..." y2="..." />
@ -959,7 +960,7 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
for (let region of BOX_MODEL_REGIONS) {
createSVGNode(this.win, {
nodeType: "polygon",
nodeType: "path",
parent: regions,
attributes: {
"class": region,
@ -1171,6 +1172,62 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
"hidden");
},
/**
* Calculate an outer quad based on the quads returned by getAdjustedQuads.
* The BoxModelHighlighter may highlight more than one boxes, so in this case
* create a new quad that "contains" all of these quads.
* This is useful to position the guides and nodeinfobar.
* This may happen if the BoxModelHighlighter is used to highlight an inline
* element that spans line breaks.
* @param {String} region The box-model region to get the outer quad for.
* @return {Object} A quad-like object {p1,p2,p3,p4,bounds}
*/
_getOuterQuad: function(region) {
let quads = this.currentQuads[region];
if (!quads.length) {
return null;
}
let quad = {
p1: {x: Infinity, y: Infinity},
p2: {x: -Infinity, y: Infinity},
p3: {x: -Infinity, y: -Infinity},
p4: {x: Infinity, y: -Infinity},
bounds: {
bottom: -Infinity,
height: 0,
left: Infinity,
right: -Infinity,
top: Infinity,
width: 0,
x: 0,
y: 0,
}
};
for (let q of quads) {
quad.p1.x = Math.min(quad.p1.x, q.p1.x);
quad.p1.y = Math.min(quad.p1.y, q.p1.y);
quad.p2.x = Math.max(quad.p2.x, q.p2.x);
quad.p2.y = Math.min(quad.p2.y, q.p2.y);
quad.p3.x = Math.max(quad.p3.x, q.p3.x);
quad.p3.y = Math.max(quad.p3.y, q.p3.y);
quad.p4.x = Math.min(quad.p4.x, q.p4.x);
quad.p4.y = Math.max(quad.p4.y, q.p4.y);
quad.bounds.bottom = Math.max(quad.bounds.bottom, q.bounds.bottom);
quad.bounds.top = Math.min(quad.bounds.top, q.bounds.top);
quad.bounds.left = Math.min(quad.bounds.left, q.bounds.left);
quad.bounds.right = Math.max(quad.bounds.right, q.bounds.right);
}
quad.bounds.x = quad.bounds.left;
quad.bounds.y = quad.bounds.top;
quad.bounds.width = quad.bounds.right - quad.bounds.left;
quad.bounds.height = quad.bounds.bottom - quad.bounds.top;
return quad;
},
/**
* Update the box model as per the current node.
*
@ -1182,8 +1239,6 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
if (this._nodeNeedsHighlighting()) {
for (let boxType of BOX_MODEL_REGIONS) {
let {p1, p2, p3, p4} = this.currentQuads[boxType];
if (this.regionFill[boxType]) {
this.markup.setAttributeForElement(this.ID_CLASS_PREFIX + boxType,
"style", "fill:" + this.regionFill[boxType]);
@ -1193,17 +1248,23 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
}
if (!this.options.showOnly || this.options.showOnly === boxType) {
// Highlighting all quads.
let path = [];
for (let {p1, p2, p3, p4} of this.currentQuads[boxType]) {
path.push("M" + p1.x + "," + p1.y + " " +
"L" + p2.x + "," + p2.y + " " +
"L" + p3.x + "," + p3.y + " " +
"L" + p4.x + "," + p4.y);
}
this.markup.setAttributeForElement(this.ID_CLASS_PREFIX + boxType,
"points", p1.x + "," + p1.y + " " +
p2.x + "," + p2.y + " " +
p3.x + "," + p3.y + " " +
p4.x + "," + p4.y);
"d", path.join(" "));
} else {
this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + boxType, "points");
this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + boxType, "d");
}
if (boxType === this.options.region && !this.options.hideGuides) {
this._showGuides(p1, p2, p3, p4);
this._showGuides(boxType);
} else if (this.options.hideGuides) {
this._hideGuides();
}
@ -1221,10 +1282,10 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
},
_nodeNeedsHighlighting: function() {
let hasNoQuads = !this.currentQuads.margin &&
!this.currentQuads.border &&
!this.currentQuads.padding &&
!this.currentQuads.content;
let hasNoQuads = !this.currentQuads.margin.length &&
!this.currentQuads.border.length &&
!this.currentQuads.padding.length &&
!this.currentQuads.content.length;
if (!this.currentNode ||
Cu.isDeadWrapper(this.currentNode) ||
this.currentNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
@ -1243,14 +1304,14 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
_getOuterBounds: function() {
for (let region of ["margin", "border", "padding", "content"]) {
let quads = this.currentQuads[region];
let quad = this._getOuterQuad(region);
if (!quads) {
if (!quad) {
// Invisible element such as a script tag.
break;
}
let {bottom, height, left, right, top, width, x, y} = quads.bounds;
let {bottom, height, left, right, top, width, x, y} = quad.bounds;
if (width > 0 || height > 0) {
return {bottom, height, left, right, top, width, x, y};
@ -1272,13 +1333,11 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
/**
* We only want to show guides for horizontal and vertical edges as this helps
* to line them up. This method finds these edges and displays a guide there.
*
* @param {DOMPoint} p1
* @param {DOMPoint} p2
* @param {DOMPoint} p3
* @param {DOMPoint} p4
* @param {String} region The region around which the guides should be shown.
*/
_showGuides: function(p1, p2, p3, p4) {
_showGuides: function(region) {
let {p1, p2, p3, p4} = this._getOuterQuad(region);
let allX = [p1.x, p2.x, p3.x, p4.x].sort((a, b) => a - b);
let allY = [p1.y, p2.y, p3.y, p4.y].sort((a, b) => a - b);
let toShowX = [];
@ -1330,14 +1389,6 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
return false;
}
let offset = GUIDE_STROKE_WIDTH / 2;
if (side === "top" || side === "left") {
point -= offset;
} else {
point += offset;
}
if (side === "top" || side === "bottom") {
this.markup.setAttributeForElement(guideId, "x1", "0");
this.markup.setAttributeForElement(guideId, "y1", point + "");
@ -1381,7 +1432,7 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
pseudos += ":" + pseudo;
}
let rect = this.currentQuads.border.bounds;
let rect = this._getOuterQuad("border").bounds;
let dim = parseFloat(rect.width.toPrecision(6)) + " \u00D7 " + parseFloat(rect.height.toPrecision(6));
let elementId = this.ID_CLASS_PREFIX + "nodeinfobar-";
@ -1630,12 +1681,15 @@ CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.proto
setIgnoreLayoutChanges(true);
// Getting the points for the transformed shape
let quad = this.currentQuads.border;
if (!quad || quad.bounds.width <= 0 || quad.bounds.height <= 0) {
let quads = this.currentQuads.border;
if (!quads.length ||
quads[0].bounds.width <= 0 || quads[0].bounds.height <= 0) {
this._hideShapes();
return null;
}
let [quad] = quads;
// Getting the points for the untransformed shape
let untransformedQuad = this.layoutHelpers.getNodeBounds(this.currentNode);
@ -1803,7 +1857,13 @@ RectHighlighter.prototype = {
let contextNode = node.ownerDocument.documentElement;
// Caculate the absolute rect based on the context node's adjusted quads.
let {bounds} = this.layoutHelpers.getAdjustedQuads(contextNode);
let quads = this.layoutHelpers.getAdjustedQuads(contextNode);
if (!quads.length) {
this.hide();
return;
}
let {bounds} = quads[0];
let x = "left:" + (bounds.x + options.rect.x) + "px;";
let y = "top:" + (bounds.y + options.rect.y) + "px;";
let width = "width:" + options.rect.width + "px;";
@ -2209,7 +2269,7 @@ GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.pro
this.offsetParent = getOffsetParent(this.currentNode);
// And the offsetParent quads.
this.parentQuads = this.layoutHelpers
.getAdjustedQuads(this.offsetParent.element, "padding");
.getAdjustedQuads(this.offsetParent.element, "padding");
let el = this.markup.getElement(this.ID_CLASS_PREFIX + "offset-parent");
@ -2219,7 +2279,7 @@ GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.pro
let isHighlighted = false;
if (this.offsetParent.element && isPositioned) {
let {p1, p2, p3, p4} = this.parentQuads;
let {p1, p2, p3, p4} = this.parentQuads[0];
let points = p1.x + "," + p1.y + " " +
p2.x + "," + p2.y + " " +
p3.x + "," + p3.y + " " +
@ -2230,7 +2290,7 @@ GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.pro
let xDelta = parseFloat(this.computedStyle.left);
let yDelta = parseFloat(this.computedStyle.top);
if (xDelta || yDelta) {
let {p1, p2, p3, p4} = this.currentQuads.margin;
let {p1, p2, p3, p4} = this.currentQuads.margin[0];
let points = (p1.x - xDelta) + "," + (p1.y - yDelta) + " " +
(p2.x - xDelta) + "," + (p2.y - yDelta) + " " +
(p3.x - xDelta) + "," + (p3.y - yDelta) + " " +
@ -2249,7 +2309,7 @@ GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.pro
updateCurrentNode: function() {
let box = this.markup.getElement(this.ID_CLASS_PREFIX + "current-node");
let {p1, p2, p3, p4} = this.currentQuads.margin;
let {p1, p2, p3, p4} = this.currentQuads.margin[0];
let attr = p1.x + "," + p1.y + " " +
p2.x + "," + p2.y + " " +
p3.x + "," + p3.y + " " +
@ -2304,7 +2364,7 @@ GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.pro
let labelEl = this.markup.getElement(id + "label-size");
let labelTextEl = this.markup.getElement(id + "label-text-size");
let {bounds} = this.currentQuads.margin;
let {bounds} = this.currentQuads.margin[0];
labelEl.setAttribute("transform", "translate(" +
(bounds.left + bounds.width/2) + " " +
@ -2318,11 +2378,11 @@ GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.pro
this.hideArrows();
// Position arrows always end at the node's margin box.
let marginBox = this.currentQuads.margin.bounds;
let marginBox = this.currentQuads.margin[0].bounds;
// But size arrows are displayed in the box that corresponds to the current
// box-sizing.
let boxSizing = this.computedStyle.boxSizing.split("-")[0];
let box = this.currentQuads[boxSizing].bounds;
let box = this.currentQuads[boxSizing][0].bounds;
// Position the side arrows which need to be visible.
// Arrows always start at the offsetParent edge, and end at the middle
@ -2341,8 +2401,8 @@ GeometryEditorHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.pro
// +------------------+----------------+
let getSideArrowStartPos = side => {
// In case an offsetParent exists and is highlighted.
if (this.parentQuads) {
return this.parentQuads.bounds[side];
if (this.parentQuads && this.parentQuads.length) {
return this.parentQuads[0].bounds[side];
}
// In case of relative positioning.