--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -495,25 +495,27 @@ InspectorPanel.prototype = {
*/
updating: function(name) {
if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
this.cancelUpdate();
}
if (!this._updateProgress) {
// Start an update in progress.
- var self = this;
+ let self = this;
this._updateProgress = {
node: this.selection.nodeFront,
outstanding: new Set(),
checkDone: function() {
if (this !== self._updateProgress) {
return;
}
- if (this.node !== self.selection.nodeFront) {
+ // Cancel update if there is no `selection` anymore.
+ // It can happen if the inspector panel is already destroyed.
+ if (!self.selection || (this.node !== self.selection.nodeFront)) {
self.cancelUpdate();
return;
}
if (this.outstanding.size !== 0) {
return;
}
self._updateProgress = null;
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -279,17 +279,21 @@
</html:div>
</html:section>
</html:div>
</tabpanel>
<tabpanel id="sidebar-panel-layoutview" class="devtools-monospace theme-sidebar inspector-tabpanel">
<html:div id="layout-container">
<html:p id="layout-header">
- <html:span id="layout-element-size"></html:span><html:span id="layout-element-position"></html:span>
+ <html:span id="layout-element-size"></html:span>
+ <html:section id="layout-position-group">
+ <html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
+ <html:span id="layout-element-position"></html:span>
+ </html:section>
</html:p>
<html:div id="layout-main">
<html:span class="layout-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
<html:div id="layout-margins" data-box="margin" title="&margin.tooltip;">
<html:span class="layout-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
<html:div id="layout-borders" data-box="border" title="&border.tooltip;">
<html:span class="layout-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -135,16 +135,17 @@ EditingSession.prototype = {
* currently loaded in the toolbox
* @param {Window} win The window containing the panel
*/
function LayoutView(inspector, win) {
this.inspector = inspector;
this.doc = win.document;
this.sizeLabel = this.doc.querySelector(".layout-size > span");
this.sizeHeadingLabel = this.doc.getElementById("layout-element-size");
+ this._geometryEditorHighlighter = null;
this.init();
}
LayoutView.prototype = {
init: function() {
this.update = this.update.bind(this);
@@ -152,16 +153,21 @@ LayoutView.prototype = {
this.inspector.selection.on("new-node-front", this.onNewSelection);
this.onNewNode = this.onNewNode.bind(this);
this.inspector.sidebar.on("layoutview-selected", this.onNewNode);
this.onSidebarSelect = this.onSidebarSelect.bind(this);
this.inspector.sidebar.on("select", this.onSidebarSelect);
+ this.onPickerStarted = this.onPickerStarted.bind(this);
+ this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
+ this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
+ this.onWillNavigate = this.onWillNavigate.bind(this);
+
this.initBoxModelHighlighter();
// Store for the different dimensions of the node.
// 'selector' refers to the element that holds the value in view.xhtml;
// 'property' is what we are measuring;
// 'value' is the computed dimension, computed in update().
this.map = {
position: {
@@ -248,16 +254,21 @@ LayoutView.prototype = {
this.onNewNode();
// Mark document as RTL or LTR:
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry);
let dir = chromeReg.isLocaleRTL("global");
let container = this.doc.getElementById("layout-container");
container.setAttribute("dir", dir ? "rtl" : "ltr");
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+
+ this.onGeometryButtonClick = this.onGeometryButtonClick.bind(this);
+ nodeGeometry.addEventListener("click", this.onGeometryButtonClick);
},
initBoxModelHighlighter: function() {
let highlightElts = this.doc.querySelectorAll("#layout-container *[title]");
this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
this.onHighlightMouseOut = this.onHighlightMouseOut.bind(this);
for (let element of highlightElts) {
@@ -371,19 +382,33 @@ LayoutView.prototype = {
destroy: function() {
let highlightElts = this.doc.querySelectorAll("#layout-container *[title]");
for (let element of highlightElts) {
element.removeEventListener("mouseover", this.onHighlightMouseOver, true);
element.removeEventListener("mouseout", this.onHighlightMouseOut, true);
}
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.removeEventListener("click", this.onGeometryButtonClick);
+
+ this.inspector.off("picker-started", this.onPickerStarted);
+
+ // Inspector Panel will destroy `markup` object on "will-navigate" event,
+ // therefore we have to check if it's still available in case LayoutView
+ // is destroyed immediately after.
+ if (this.inspector.markup) {
+ this.inspector.markup.off("leave", this.onMarkupViewLeave);
+ this.inspector.markup.off("node-hover", this.onMarkupViewNodeHover);
+ }
+
this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
this.inspector.selection.off("new-node-front", this.onNewSelection);
this.inspector.sidebar.off("select", this.onSidebarSelect);
+ this.inspector._target.off("will-navigate", this.onWillNavigate);
this.sizeHeadingLabel = null;
this.sizeLabel = null;
this.inspector = null;
this.doc = null;
if (this.reflowFront) {
this.untrackReflows();
@@ -396,20 +421,22 @@ LayoutView.prototype = {
this.setActive(sidebar === "layoutview");
},
/**
* Selection 'new-node-front' event handler.
*/
onNewSelection: function() {
let done = this.inspector.updating("layoutview");
- this.onNewNode().then(done, err => {
- console.error(err);
- done();
- });
+ this.onNewNode()
+ .then(() => this.hideGeometryEditor())
+ .then(done, (err) => {
+ console.error(err);
+ done();
+ }).catch(console.error);
},
/**
* @return a promise that resolves when the view has been updated
*/
onNewNode: function() {
this.setActive(this.isViewVisibleAndNodeValid());
return this.update();
@@ -427,16 +454,43 @@ LayoutView.prototype = {
onlyRegionArea: true
});
},
onHighlightMouseOut: function() {
this.hideBoxModel();
},
+ onGeometryButtonClick: function({target}) {
+ if (target.hasAttribute("checked")) {
+ target.removeAttribute("checked");
+ this.hideGeometryEditor();
+ } else {
+ target.setAttribute("checked", "true");
+ this.showGeometryEditor();
+ }
+ },
+
+ onPickerStarted: function() {
+ this.hideGeometryEditor();
+ },
+
+ onMarkupViewLeave: function() {
+ this.showGeometryEditor(true);
+ },
+
+ onMarkupViewNodeHover: function() {
+ this.hideGeometryEditor(false);
+ },
+
+ onWillNavigate: function() {
+ this._geometryEditorHighlighter.release().catch(console.error);
+ this._geometryEditorHighlighter = null;
+ },
+
/**
* Stop tracking reflows and hide all values when no node is selected or the
* layout-view is hidden, otherwise track reflows and show values.
* @param {Boolean} isActive
*/
setActive: function(isActive) {
if (isActive === this.isActive) {
return;
@@ -454,27 +508,29 @@ LayoutView.prototype = {
},
/**
* Compute the dimensions of the node and update the values in
* the layoutview/view.xhtml document.
* @return a promise that will be resolved when complete.
*/
update: function() {
- let lastRequest = Task.spawn((function*() {
+ let lastRequest = Task.spawn((function* () {
if (!this.isViewVisibleAndNodeValid()) {
return null;
}
let node = this.inspector.selection.nodeFront;
let layout = yield this.inspector.pageStyle.getLayout(node, {
autoMargins: this.isActive
});
let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
+ yield this.updateGeometryButton();
+
// If a subsequent request has been made, wait for that one instead.
if (this._lastRequest != lastRequest) {
return this._lastRequest;
}
this._lastRequest = null;
let width = layout.width;
let height = layout.height;
@@ -543,17 +599,17 @@ LayoutView.prototype = {
let newValue = width + "\u00D7" + height;
if (this.sizeLabel.textContent != newValue) {
this.sizeLabel.textContent = newValue;
}
this.elementRules = styleEntries.map(e => e.rule);
this.inspector.emit("layoutview-updated");
- }).bind(this)).then(null, console.error);
+ }).bind(this)).catch(console.error);
this._lastRequest = lastRequest;
return this._lastRequest;
},
/**
* Update the text in the tooltip shown when hovering over a value to provide
* information about the source CSS rule that sets this value.
@@ -603,16 +659,87 @@ LayoutView.prototype = {
* Hide the box-model highlighter on the currently selected element
*/
hideBoxModel: function() {
let toolbox = this.inspector.toolbox;
toolbox.highlighterUtils.unhighlight();
},
+ /**
+ * Show the geometry editor highlighter on the currently selected element
+ * @param {Boolean} [showOnlyIfActive=false]
+ * Indicates if the Geometry Editor should be shown only if it's active but
+ * hidden.
+ */
+ showGeometryEditor: function(showOnlyIfActive = false) {
+ let toolbox = this.inspector.toolbox;
+ let nodeFront = this.inspector.selection.nodeFront;
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ let isActive = nodeGeometry.hasAttribute("checked");
+
+ if (showOnlyIfActive && !isActive) {
+ return;
+ }
+
+ if (this._geometryEditorHighlighter) {
+ this._geometryEditorHighlighter.show(nodeFront).catch(console.error);
+ return;
+ }
+
+ // instantiate Geometry Editor highlighter
+ toolbox.highlighterUtils
+ .getHighlighterByType("GeometryEditorHighlighter").then(highlighter => {
+ highlighter.show(nodeFront).catch(console.error);
+ this._geometryEditorHighlighter = highlighter;
+
+ // Hide completely the geometry editor if the picker is clicked
+ toolbox.on("picker-started", this.onPickerStarted);
+
+ // Temporary hide the geometry editor
+ this.inspector.markup.on("leave", this.onMarkupViewLeave);
+ this.inspector.markup.on("node-hover", this.onMarkupViewNodeHover);
+
+ // Release the actor on will-navigate event
+ this.inspector._target.once("will-navigate", this.onWillNavigate);
+ });
+ },
+
+ /**
+ * Hide the geometry editor highlighter on the currently selected element
+ * @param {Boolean} [updateButton=true]
+ * Indicates if the Geometry Editor's button needs to be unchecked too
+ */
+ hideGeometryEditor: function(updateButton = true) {
+ if (this._geometryEditorHighlighter) {
+ this._geometryEditorHighlighter.hide().catch(console.error);
+ }
+
+ if (updateButton) {
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Update the visibility and the state of the geometry editor button,
+ * based on the selected node.
+ */
+ updateGeometryButton: Task.async(function* () {
+ let node = this.inspector.selection.nodeFront;
+ let isEditable = false;
+
+ if (node) {
+ isEditable = yield this.inspector.pageStyle.isPositionEditable(node);
+ }
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.style.visibility = isEditable ? "visible" : "hidden";
+ }),
+
manageOverflowingText: function(span) {
let classList = span.parentNode.classList;
if (classList.contains("layout-left") ||
classList.contains("layout-right")) {
let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
classList.toggle("layout-rotate", force);
}
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -204,16 +204,18 @@ MarkupView.prototype = {
if (this._hoveredNode !== container.node) {
if (container.node.nodeType !== Ci.nsIDOMNode.TEXT_NODE) {
this._showBoxModel(container.node);
} else {
this._hideBoxModel();
}
}
this._showContainerAsHovered(container.node);
+
+ this.emit("node-hover");
},
/**
* Executed on each mouse-move while a node is being dragged in the view.
* Auto-scrolls the view to reveal nodes below the fold to drop the dragged
* node in.
*/
_autoScroll: function(event) {
@@ -336,16 +338,18 @@ MarkupView.prototype = {
return;
}
this._hideBoxModel(true);
if (this._hoveredNode) {
this.getContainer(this._hoveredNode).hovered = false;
}
this._hoveredNode = null;
+
+ this.emit("leave");
},
/**
* Show the box model highlighter on a given node front
*
* @param {NodeFront} nodeFront
* The node to show the highlighter for
* @return {Promise} Resolves when the highlighter for this nodeFront is
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -52,16 +52,17 @@ support-files =
[browser_inspector_highlighter-csstransform_01.js]
[browser_inspector_highlighter-csstransform_02.js]
[browser_inspector_highlighter-embed.js]
[browser_inspector_highlighter-geometry_01.js]
[browser_inspector_highlighter-geometry_02.js]
[browser_inspector_highlighter-geometry_03.js]
[browser_inspector_highlighter-geometry_04.js]
[browser_inspector_highlighter-geometry_05.js]
+[browser_inspector_highlighter-geometry_06.js]
[browser_inspector_highlighter-hover_01.js]
[browser_inspector_highlighter-hover_02.js]
[browser_inspector_highlighter-hover_03.js]
[browser_inspector_highlighter-iframes_01.js]
[browser_inspector_highlighter-iframes_02.js]
[browser_inspector_highlighter-inline.js]
[browser_inspector_highlighter-keybinding_01.js]
[browser_inspector_highlighter-keybinding_02.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-geometry_06.js
@@ -0,0 +1,166 @@
+/* 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 the geometry editor resizes properly an element on all sides,
+// with different unit measures, and that arrow/handlers are updated correctly.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter-geometry_01.html";
+const ID = "geometry-editor-";
+const HIGHLIGHTER_TYPE = "GeometryEditorHighlighter";
+
+const SIDES = ["top", "right", "bottom", "left"];
+
+// The object below contains all the tests for this unit test.
+// The property's name is the test's description, that points to an
+// object contains the steps (what side of the geometry editor to drag,
+// the amount of pixels) and the expectation.
+const TESTS = {
+ "Drag top's handler along x and y, south-east direction": {
+ "expects": "Only y axis is used to updated the top's element value",
+ "drag": "top",
+ "by": {x: 10, y: 10}
+ },
+ "Drag right's handler along x and y, south-east direction": {
+ "expects": "Only x axis is used to updated the right's element value",
+ "drag": "right",
+ "by": {x: 10, y: 10}
+ },
+ "Drag bottom's handler along x and y, south-east direction": {
+ "expects": "Only y axis is used to updated the bottom's element value",
+ "drag": "bottom",
+ "by": {x: 10, y: 10}
+ },
+ "Drag left's handler along x and y, south-east direction": {
+ "expects": "Only y axis is used to updated the left's element value",
+ "drag": "left",
+ "by": {x: 10, y: 10}
+ },
+ "Drag top's handler along x and y, north-west direction": {
+ "expects": "Only y axis is used to updated the top's element value",
+ "drag": "top",
+ "by": {x: -20, y: -20}
+ },
+ "Drag right's handler along x and y, north-west direction": {
+ "expects": "Only x axis is used to updated the right's element value",
+ "drag": "right",
+ "by": {x: -20, y: -20}
+ },
+ "Drag bottom's handler along x and y, north-west direction": {
+ "expects": "Only y axis is used to updated the bottom's element value",
+ "drag": "bottom",
+ "by": {x: -20, y: -20}
+ },
+ "Drag left's handler along x and y, north-west direction": {
+ "expects": "Only y axis is used to updated the left's element value",
+ "drag": "left",
+ "by": {x: -20, y: -20}
+ }
+};
+
+add_task(function* () {
+ let inspector = yield openInspectorForURL(TEST_URL);
+ let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
+
+ helper.prefix = ID;
+
+ let { show, hide, finalize } = helper;
+
+ info("Showing the highlighter");
+ yield show("#node2");
+
+ for (let desc in TESTS) {
+ yield executeTest(helper, desc, TESTS[desc]);
+ }
+
+ info("Hiding the highlighter");
+ yield hide();
+ yield finalize();
+});
+
+function* executeTest(helper, desc, data) {
+ info(desc);
+
+ ok((yield areElementAndHighlighterMovedCorrectly(
+ helper, data.drag, data.by)), data.expects);
+}
+
+function* areElementAndHighlighterMovedCorrectly(helper, side, by) {
+ let { mouse, reflow, highlightedNode } = helper;
+
+ let {x, y} = yield getHandlerCoords(helper, side);
+
+ let dx = x + by.x;
+ let dy = y + by.y;
+
+ let beforeDragStyle = yield highlightedNode.getComputedStyle();
+
+ // simulate drag & drop
+ yield mouse.down(x, y);
+ yield mouse.move(dx, dy);
+ yield mouse.up();
+
+ yield reflow();
+
+ info(`Checking ${side} handler is moved correctly`);
+ yield isHandlerPositionUpdated(helper, side, x, y, by);
+
+ let delta = (side === "left" || side === "right") ? by.x : by.y;
+ delta = delta * ((side === "right" || side === "bottom") ? -1 : 1);
+
+ info("Checking element's sides are correct after drag & drop");
+ return yield areElementSideValuesCorrect(highlightedNode, beforeDragStyle,
+ side, delta);
+}
+
+function* isHandlerPositionUpdated(helper, name, x, y, by) {
+ let {x: afterDragX, y: afterDragY} = yield getHandlerCoords(helper, name);
+
+ if (name === "left" || name === "right") {
+ is(afterDragX, x + by.x,
+ `${name} handler's x axis updated.`);
+ is(afterDragY, y,
+ `${name} handler's y axis unchanged.`);
+ } else {
+ is(afterDragX, x,
+ `${name} handler's x axis unchanged.`);
+ is(afterDragY, y + by.y,
+ `${name} handler's y axis updated.`);
+ }
+}
+
+function* areElementSideValuesCorrect(node, beforeDragStyle, name, delta) {
+ let afterDragStyle = yield node.getComputedStyle();
+ let isSideCorrect = true;
+
+ for (let side of SIDES) {
+ let afterValue = Math.round(parseFloat(afterDragStyle[side].value));
+ let beforeValue = Math.round(parseFloat(beforeDragStyle[side].value));
+
+ if (side === name) {
+ // `isSideCorrect` is used only as test's return value, not to perform
+ // the actual test, because with `is` instead of `ok` we gather more
+ // information in case of failure
+ isSideCorrect = isSideCorrect && (afterValue === beforeValue + delta);
+
+ is(afterValue, beforeValue + delta,
+ `${side} is updated.`);
+ } else {
+ isSideCorrect = isSideCorrect && (afterValue === beforeValue);
+
+ is(afterValue, beforeValue,
+ `${side} is unchaged.`);
+ }
+ }
+
+ return isSideCorrect;
+}
+
+function* getHandlerCoords({getElementAttribute}, side) {
+ return {
+ x: Math.round(yield getElementAttribute("handler-" + side, "cx")),
+ y: Math.round(yield getElementAttribute("handler-" + side, "cy"))
+ };
+}
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -217,16 +217,17 @@ devtools.jar:
skin/images/itemToggle@2x.png (themes/images/itemToggle@2x.png)
skin/images/itemArrow-dark-rtl.svg (themes/images/itemArrow-dark-rtl.svg)
skin/images/itemArrow-dark-ltr.svg (themes/images/itemArrow-dark-ltr.svg)
skin/images/itemArrow-rtl.svg (themes/images/itemArrow-rtl.svg)
skin/images/itemArrow-ltr.svg (themes/images/itemArrow-ltr.svg)
skin/images/noise.png (themes/images/noise.png)
skin/images/dropmarker.svg (themes/images/dropmarker.svg)
skin/layout.css (themes/layout.css)
+ skin/images/geometry-editor.svg (themes/images/geometry-editor.svg)
skin/images/debugger-pause.png (themes/images/debugger-pause.png)
skin/images/debugger-pause@2x.png (themes/images/debugger-pause@2x.png)
skin/images/debugger-play.png (themes/images/debugger-play.png)
skin/images/debugger-play@2x.png (themes/images/debugger-play@2x.png)
skin/images/fast-forward.png (themes/images/fast-forward.png)
skin/images/fast-forward@2x.png (themes/images/fast-forward@2x.png)
skin/images/rewind.png (themes/images/rewind.png)
skin/images/rewind@2x.png (themes/images/rewind@2x.png)
--- a/devtools/client/locales/en-US/layoutview.dtd
+++ b/devtools/client/locales/en-US/layoutview.dtd
@@ -11,13 +11,18 @@
- You want to make that choice consistent across the developer tools.
- A good criteria is the language in which you'd find the best
- documentation on web development on the web. -->
<!-- LOCALIZATION NOTE (*.tooltip): These tooltips are not regular tooltips.
- The text appears on the bottom right corner of the layout view when
- the corresponding box is hovered. -->
-<!ENTITY layoutViewTitle "Box Model">
-<!ENTITY margin.tooltip "margin">
-<!ENTITY border.tooltip "border">
-<!ENTITY padding.tooltip "padding">
-<!ENTITY content.tooltip "content">
+<!ENTITY layoutViewTitle "Box Model">
+<!ENTITY margin.tooltip "margin">
+<!ENTITY border.tooltip "border">
+<!ENTITY padding.tooltip "padding">
+<!ENTITY content.tooltip "content">
+
+<!-- LOCALIZATION NOTE: This label is displayed as a tooltip that appears when
+ - hovering over the button that allows users to edit the position of an
+ - element in the page. -->
+<!ENTITY geometry.button.tooltip "Edit position">
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/geometry-editor.svg
@@ -0,0 +1,4 @@
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="#babec3">
+ <path d="M14,8 L12,8 L12,11.25 L12,12 L11.5,12 L3.5,12 L3,12 L3,11.75 L3,11.5 L3,8 L1,8 L1,8 L1,8.5 L1,9 L0,9 L0,8.5 L0,6.5 L0,6 L1,6 L1,6.5 L1,7 L3,7 L3,3.5 L3,3 L3.72222222,3 L3.72222222,3 L10.5555556,3 L11,3 L11,4 L10.5555556,4 L4,4 L4,11 L11,11 L11,3.5 L11,3 L12,3 L12,3.5 L12,7 L14,7 L14,6.5 L14,6 L15,6 L15,6.5 L15,8.5 L15,9 L14,9 L14,8.5 L14,8 Z M8,14 L8.5,14 L9,14 L9,15 L8.5,15 L6.5,15 L6,15 L6,14 L6.5,14 L7,14 L7,11.5 L7,11 L8,11 L8,11.5 L8,14 Z M7,1 L6.5,1 L6,1 L6,0 L6.5,0 L8.5,0 L9,0 L9,1 L8.5,1 L8,1 L8,3.5 L8,4 L7,4 L7,3.5 L7,1 L7,1 Z"/>
+ <path d="M3.5,9 C4.32842712,9 5,8.32842712 5,7.5 C5,6.67157288 4.32842712,6 3.5,6 C2.67157288,6 2,6.67157288 2,7.5 C2,8.32842712 2.67157288,9 3.5,9 Z M7.5,13 C8.32842712,13 9,12.3284271 9,11.5 C9,10.6715729 8.32842712,10 7.5,10 C6.67157288,10 6,10.6715729 6,11.5 C6,12.3284271 6.67157288,13 7.5,13 Z M11.5,9 C12.3284271,9 13,8.32842712 13,7.5 C13,6.67157288 12.3284271,6 11.5,6 C10.6715729,6 10,6.67157288 10,7.5 C10,8.32842712 10.6715729,9 11.5,9 Z M7.5,5 C8.32842712,5 9,4.32842712 9,3.5 C9,2.67157288 8.32842712,2 7.5,2 C6.67157288,2 6,2.67157288 6,3.5 C6,4.32842712 6.67157288,5 7.5,5 Z"/>
+</svg>
\ No newline at end of file
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -331,8 +331,21 @@
/* Hide all values when the view is inactive */
#layout-container.inactive > #layout-header > #layout-element-position,
#layout-container.inactive > #layout-header > #layout-element-size,
#layout-container.inactive > #layout-main > p {
visibility: hidden;
}
+
+#layout-position-group {
+ display: flex;
+ align-items: center;
+}
+
+#layout-geometry-editor {
+ visibility: hidden;
+}
+
+#layout-geometry-editor::before {
+ background: url(images/geometry-editor.svg) no-repeat center center / 16px 16px;
+}
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -24,16 +24,20 @@
'pointer-events:auto;' on its container element. */
pointer-events: none;
}
:-moz-native-anonymous .highlighter-container [hidden] {
display: none;
}
+:-moz-native-anonymous .highlighter-container [dragging] {
+ cursor: grabbing;
+}
+
/* Box model highlighter */
:-moz-native-anonymous .box-model-regions {
opacity: 0.6;
}
/* Box model regions can be faded (see the onlyRegionArea option in
highlighters.js) in order to only display certain regions. */
@@ -202,16 +206,17 @@
}
/* Element geometry highlighter */
:-moz-native-anonymous .geometry-editor-root {
/* The geometry editor can be interacted with, so it needs to react to
pointer events */
pointer-events: auto;
+ -moz-user-select: none;
}
:-moz-native-anonymous .geometry-editor-offset-parent {
stroke: #08c;
shape-rendering: crispEdges;
stroke-dasharray: 5 3;
fill: transparent;
}
@@ -223,16 +228,45 @@
opacity: 0.6;
}
:-moz-native-anonymous .geometry-editor-arrow {
stroke: #08c;
shape-rendering: crispEdges;
}
+:-moz-native-anonymous .geometry-editor-root circle {
+ stroke: #08c;
+ fill: #87ceeb;
+}
+
+:-moz-native-anonymous .geometry-editor-handler-top,
+:-moz-native-anonymous .geometry-editor-handler-bottom {
+ cursor: ns-resize;
+}
+
+:-moz-native-anonymous .geometry-editor-handler-right,
+:-moz-native-anonymous .geometry-editor-handler-left {
+ cursor: ew-resize;
+}
+
+:-moz-native-anonymous [dragging] .geometry-editor-handler-top,
+:-moz-native-anonymous [dragging] .geometry-editor-handler-right,
+:-moz-native-anonymous [dragging] .geometry-editor-handler-bottom,
+:-moz-native-anonymous [dragging] .geometry-editor-handler-left {
+ cursor: grabbing;
+}
+
+:-moz-native-anonymous .geometry-editor-handler-top.dragging,
+:-moz-native-anonymous .geometry-editor-handler-right.dragging,
+:-moz-native-anonymous .geometry-editor-handler-bottom.dragging,
+:-moz-native-anonymous .geometry-editor-handler-left.dragging {
+ fill: #08c;
+}
+
:-moz-native-anonymous .geometry-editor-label-bubble {
fill: hsl(214,13%,24%);
shape-rendering: crispEdges;
}
:-moz-native-anonymous .geometry-editor-label-text {
fill: hsl(216,33%,97%);
font: message-box;
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -457,16 +457,18 @@ var CustomHighlighterActor = exports.Cus
},
destroy: function() {
protocol.Actor.prototype.destroy.call(this);
this.finalize();
this._inspector = null;
},
+ release: method(function() {}, { release: true }),
+
/**
* Show the highlighter.
* This calls through to the highlighter instance's |show(node, options)|
* method.
*
* Most custom highlighters are made to highlight DOM nodes, hence the first
* NodeActor argument (NodeActor as in
* devtools/server/actor/inspector).
--- a/devtools/server/actors/highlighters/geometry-editor.js
+++ b/devtools/server/actors/highlighters/geometry-editor.js
@@ -10,16 +10,22 @@ const {
CanvasFrameAnonymousContentHelper, getCSSStyleRules, getComputedStyle,
createSVGNode, createNode } = require("./utils/markup");
const { setIgnoreLayoutChanges,
getAdjustedQuads } = require("devtools/shared/layout/utils");
const GEOMETRY_LABEL_SIZE = 6;
+// List of all DOM Events subscribed directly to the document from the
+// Geometry Editor highlighter
+const DOM_EVENTS = ["mousemove", "mouseup", "pagehide"];
+
+const _dragging = Symbol("geometry/dragging");
+
/**
* Element geometry properties helper that gives names of position and size
* properties.
*/
var GeoProp = {
SIDES: ["top", "right", "bottom", "left"],
SIZES: ["width", "height"],
@@ -108,16 +114,85 @@ function getOffsetParent(node) {
return {
element: offsetParent,
dimension: {width, height}
};
}
/**
+ * Get the list of geometry properties that are actually set on the provided
+ * node.
+ *
+ * @param {nsIDOMNode} node The node to analyze.
+ * @return {Map} A map indexed by property name and where the value is an
+ * object having the cssRule property.
+ */
+function getDefinedGeometryProperties(node) {
+ let props = new Map();
+ if (!node) {
+ return props;
+ }
+
+ // Get the list of css rules applying to the current node.
+ let cssRules = getCSSStyleRules(node);
+ for (let i = 0; i < cssRules.Count(); i++) {
+ let rule = cssRules.GetElementAt(i);
+ for (let name of GeoProp.allProps()) {
+ let value = rule.style.getPropertyValue(name);
+ if (value && value !== "auto") {
+ // getCSSStyleRules returns rules ordered from least to most specific
+ // so just override any previous properties we have set.
+ props.set(name, {
+ cssRule: rule
+ });
+ }
+ }
+ }
+
+ // Go through the inline styles last, only if the node supports inline style
+ // (e.g. pseudo elements don't have a style property)
+ if (node.style) {
+ for (let name of GeoProp.allProps()) {
+ let value = node.style.getPropertyValue(name);
+ if (value && value !== "auto") {
+ props.set(name, {
+ // There's no cssRule to store here, so store the node instead since
+ // node.style exists.
+ cssRule: node
+ });
+ }
+ }
+ }
+
+ // Post-process the list for invalid properties. This is done after the fact
+ // because of cases like relative positioning with both top and bottom where
+ // only top will actually be used, but both exists in css rules and computed
+ // styles.
+ let { position } = getComputedStyle(node);
+ for (let [name] of props) {
+ // Top/left/bottom/right on static positioned elements have no effect.
+ if (position === "static" && GeoProp.SIDES.indexOf(name) !== -1) {
+ props.delete(name);
+ }
+
+ // Bottom/right on relative positioned elements are only used if top/left
+ // are not defined.
+ let hasRightAndLeft = name === "right" && props.has("left");
+ let hasBottomAndTop = name === "bottom" && props.has("top");
+ if (position === "relative" && (hasRightAndLeft || hasBottomAndTop)) {
+ props.delete(name);
+ }
+ }
+
+ return props;
+}
+exports.getDefinedGeometryProperties = getDefinedGeometryProperties;
+
+/**
* The GeometryEditor highlights an elements's top, left, bottom, right, width
* and height dimensions, when they are set.
*
* To determine if an element has a set size and position, the highlighter lists
* the CSS rules that apply to the element and checks for the top, left, bottom,
* right, width and height properties.
* The highlighter won't be shown if the element doesn't have any of these
* properties set, but will be shown when at least 1 property is defined.
@@ -133,33 +208,48 @@ function getOffsetParent(node) {
function GeometryEditorHighlighter(highlighterEnv) {
AutoRefreshHighlighter.call(this, highlighterEnv);
// The list of element geometry properties that can be set.
this.definedProperties = new Map();
this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
this._buildMarkup.bind(this));
+
+ let { pageListenerTarget } = this.highlighterEnv;
+
+ // Register the geometry editor instance to all events we're interested in.
+ DOM_EVENTS.forEach(type => pageListenerTarget.addEventListener(type, this));
+
+ // Register the mousedown event for each Geometry Editor's handler.
+ // Those events are automatically removed when the markup is destroyed.
+ let onMouseDown = this.handleEvent.bind(this);
+
+ for (let side of GeoProp.SIDES) {
+ this.getElement("handler-" + side)
+ .addEventListener("mousedown", onMouseDown);
+ }
}
GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
typeName: "GeometryEditorHighlighter",
ID_CLASS_PREFIX: "geometry-editor-",
_buildMarkup: function() {
let container = createNode(this.win, {
attributes: {"class": "highlighter-container"}
});
let root = createNode(this.win, {
parent: container,
attributes: {
"id": "root",
- "class": "root"
+ "class": "root",
+ "hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
let svg = createSVGNode(this.win, {
nodeType: "svg",
parent: root,
attributes: {
@@ -189,29 +279,42 @@ GeometryEditorHighlighter.prototype = ex
attributes: {
"class": "current-node",
"id": "current-node",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
- // Build the 4 side arrows and labels.
+ // Build the 4 side arrows, handlers and labels.
for (let name of GeoProp.SIDES) {
createSVGNode(this.win, {
nodeType: "line",
parent: svg,
attributes: {
"class": "arrow " + name,
"id": "arrow-" + name,
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
+ createSVGNode(this.win, {
+ nodeType: "circle",
+ parent: svg,
+ attributes: {
+ "class": "handler-" + name,
+ "id": "handler-" + name,
+ "r": "4",
+ "data-side": name,
+ "hidden": "true"
+ },
+ prefix: this.ID_CLASS_PREFIX
+ });
+
// Labels are positioned by using a translated <g>. This group contains
// a path and text that are themselves positioned using another translated
// <g>. This is so that the label arrow points at the 0,0 coordinates of
// parent <g>.
let labelG = createSVGNode(this.win, {
nodeType: "g",
parent: svg,
attributes: {
@@ -251,176 +354,173 @@ GeometryEditorHighlighter.prototype = ex
"id": "label-text-" + name,
"x": GeoProp.isHorizontal(name) ? "30" : "35",
"y": "10"
},
prefix: this.ID_CLASS_PREFIX
});
}
- // Build the width/height label and resize handle.
- let labelSizeG = createSVGNode(this.win, {
- nodeType: "g",
- parent: svg,
- attributes: {
- "id": "label-size",
- "hidden": "true"
- },
- prefix: this.ID_CLASS_PREFIX
- });
-
- let subSizeG = createSVGNode(this.win, {
- nodeType: "g",
- parent: labelSizeG,
- attributes: {
- "transform": "translate(-50 -10)"
- }
- });
-
- createSVGNode(this.win, {
- nodeType: "path",
- parent: subSizeG,
- attributes: {
- "class": "label-bubble",
- "d": "M0 0 L100 0 L100 20 L0 20z"
- },
- prefix: this.ID_CLASS_PREFIX
- });
-
- createSVGNode(this.win, {
- nodeType: "text",
- parent: subSizeG,
- attributes: {
- "class": "label-text",
- "id": "label-text-size",
- "x": "50",
- "y": "10"
- },
- prefix: this.ID_CLASS_PREFIX
- });
-
return container;
},
destroy: function() {
+ // Avoiding exceptions if `destroy` is called multiple times; and / or the
+ // highlighter environment was already destroyed.
+ if (!this.highlighterEnv) {
+ return;
+ }
+
+ let { pageListenerTarget } = this.highlighterEnv;
+
+ DOM_EVENTS.forEach(type =>
+ pageListenerTarget.removeEventListener(type, this));
+
AutoRefreshHighlighter.prototype.destroy.call(this);
this.markup.destroy();
this.definedProperties.clear();
this.definedProperties = null;
this.offsetParent = null;
},
- getElement: function(id) {
- return this.markup.getElement(this.ID_CLASS_PREFIX + id);
- },
-
- /**
- * Get the list of geometry properties that are actually set on the current
- * node.
- * @return {Map} A map indexed by property name and where the value is an
- * object having the cssRule property.
- */
- getDefinedGeometryProperties: function() {
- let props = new Map();
- if (!this.currentNode) {
- return props;
- }
-
- // Get the list of css rules applying to the current node.
- let cssRules = getCSSStyleRules(this.currentNode);
- for (let i = 0; i < cssRules.Count(); i++) {
- let rule = cssRules.GetElementAt(i);
- for (let name of GeoProp.allProps()) {
- let value = rule.style.getPropertyValue(name);
- if (value && value !== "auto") {
- // getCSSStyleRules returns rules ordered from least to most specific
- // so just override any previous properties we have set.
- props.set(name, {
- cssRule: rule
- });
- }
- }
+ handleEvent: function(event, id) {
+ // No event handling if the highlighter is hidden
+ if (this.getElement("root").hasAttribute("hidden")) {
+ return;
}
- // Go through the inline styles last.
- for (let name of GeoProp.allProps()) {
- let value = this.currentNode.style.getPropertyValue(name);
- if (value && value !== "auto") {
- props.set(name, {
- // There's no cssRule to store here, so store the node instead since
- // node.style exists.
- cssRule: this.currentNode
- });
- }
- }
+ const { type, pageX, pageY } = event;
+
+ switch (type) {
+ case "pagehide":
+ this.destroy();
+ break;
+ case "mousedown":
+ // The mousedown event is intended only for the handler
+ if (!id) {
+ return;
+ }
+
+ let handlerSide = this.markup.getElement(id).getAttribute("data-side");
+
+ if (handlerSide) {
+ let side = handlerSide;
+ let sideProp = this.definedProperties.get(side);
+
+ if (!sideProp) {
+ return;
+ }
+
+ let value = sideProp.cssRule.style.getPropertyValue(side);
+ let computedValue = this.computedStyle.getPropertyValue(side);
+
+ let [unit] = value.match(/[^\d]+$/) || [""];
+
+ value = parseFloat(value);
+
+ let ratio = (value / parseFloat(computedValue)) || 1;
+ let dir = GeoProp.isInverted(side) ? -1 : 1;
+
+ // Store all the initial values needed for drag & drop
+ this[_dragging] = {
+ side,
+ value,
+ unit,
+ x: pageX,
+ y: pageY,
+ inc: ratio * dir
+ };
- // Post-process the list for invalid properties. This is done after the fact
- // because of cases like relative positioning with both top and bottom where
- // only top will actually be used, but both exists in css rules and computed
- // styles.
- for (let [name] of props) {
- let pos = this.computedStyle.position;
+ this.getElement("handler-" + side).classList.add("dragging");
+ }
+
+ this.getElement("root").setAttribute("dragging", "true");
+ break;
+ case "mouseup":
+ // If we're dragging, drop it.
+ if (this[_dragging]) {
+ let { side } = this[_dragging];
+ this.getElement("root").removeAttribute("dragging");
+ this.getElement("handler-" + side).classList.remove("dragging");
+ this[_dragging] = null;
+ }
+ break;
+ case "mousemove":
+ if (!this[_dragging]) {
+ return;
+ }
- // Top/left/bottom/right on static positioned elements have no effect.
- if (pos === "static" && GeoProp.SIDES.indexOf(name) !== -1) {
- props.delete(name);
- }
+ let { side, x, y, value, unit, inc } = this[_dragging];
+ let sideProps = this.definedProperties.get(side);
+
+ if (!sideProps) {
+ return;
+ }
+
+ let delta = (GeoProp.isHorizontal(side) ? pageX - x : pageY - y) * inc;
- // Bottom/right on relative positioned elements are only used if top/left
- // are not defined.
- let hasRightAndLeft = name === "right" && props.has("left");
- let hasBottomAndTop = name === "bottom" && props.has("top");
- if (pos === "relative" && (hasRightAndLeft || hasBottomAndTop)) {
- props.delete(name);
- }
+ // The inline style has usually the priority over any other CSS rule
+ // set in stylesheets. However, if a rule has `!important` keyword,
+ // it will override the inline style too. To ensure Geometry Editor
+ // will always update the element, we have to add `!important` as
+ // well.
+ this.currentNode.style.setProperty(
+ side, (value + delta) + unit, "important");
+
+ break;
}
+ },
- return props;
+ getElement: function(id) {
+ return this.markup.getElement(this.ID_CLASS_PREFIX + id);
},
_show: function() {
this.computedStyle = getComputedStyle(this.currentNode);
let pos = this.computedStyle.position;
// XXX: sticky positioning is ignored for now. To be implemented next.
if (pos === "sticky") {
this.hide();
return false;
}
let hasUpdated = this._update();
if (!hasUpdated) {
this.hide();
return false;
}
+
+ this.getElement("root").removeAttribute("hidden");
+
return true;
},
_update: function() {
// At each update, the position or/and size may have changed, so get the
// list of defined properties, and re-position the arrows and highlighters.
- this.definedProperties = this.getDefinedGeometryProperties();
+ this.definedProperties = getDefinedGeometryProperties(this.currentNode);
if (!this.definedProperties.size) {
console.warn("The element does not have editable geometry properties");
return false;
}
setIgnoreLayoutChanges(true);
// Update the highlighters and arrows.
this.updateOffsetParent();
this.updateCurrentNode();
this.updateArrows();
- this.updateSize();
// Avoid zooming the arrows when content is zoomed.
- this.markup.scaleRootElement(this.currentNode, this.ID_CLASS_PREFIX + "root");
+ let node = this.currentNode;
+ this.markup.scaleRootElement(node, this.ID_CLASS_PREFIX + "root");
- setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+ setIgnoreLayoutChanges(false, node.ownerDocument.documentElement);
return true;
},
/**
* Update the offset parent rectangle.
* There are 3 different cases covered here:
* - the node is absolutely/fixed positioned, and an offsetParent is defined
* (i.e. it's not just positioned in the viewport): the offsetParent node
@@ -483,62 +583,32 @@ GeometryEditorHighlighter.prototype = ex
p4.x + "," + p4.y;
box.setAttribute("points", attr);
box.removeAttribute("hidden");
},
_hide: function() {
setIgnoreLayoutChanges(true);
+ this.getElement("root").setAttribute("hidden", "true");
this.getElement("current-node").setAttribute("hidden", "true");
this.getElement("offset-parent").setAttribute("hidden", "true");
this.hideArrows();
- this.hideSize();
this.definedProperties.clear();
- setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+ setIgnoreLayoutChanges(false,
+ this.currentNode.ownerDocument.documentElement);
},
hideArrows: function() {
for (let side of GeoProp.SIDES) {
this.getElement("arrow-" + side).setAttribute("hidden", "true");
this.getElement("label-" + side).setAttribute("hidden", "true");
- }
- },
-
- hideSize: function() {
- this.getElement("label-size").setAttribute("hidden", "true");
- },
-
- updateSize: function() {
- this.hideSize();
-
- let labels = [];
- let width = this.definedProperties.get("width");
- let height = this.definedProperties.get("height");
-
- if (width) {
- labels.push("↔ " + width.cssRule.style.getPropertyValue("width"));
- }
- if (height) {
- labels.push("↕ " + height.cssRule.style.getPropertyValue("height"));
- }
-
- if (labels.length) {
- let labelEl = this.getElement("label-size");
- let labelTextEl = this.getElement("label-text-size");
-
- let {bounds} = this.currentQuads.margin[0];
-
- labelEl.setAttribute("transform", "translate(" +
- (bounds.left + bounds.width / 2) + " " +
- (bounds.top + bounds.height / 2) + ")");
- labelEl.removeAttribute("hidden");
- labelTextEl.setTextContent(labels.join(" "));
+ this.getElement("handler-" + side).setAttribute("hidden", "true");
}
},
updateArrows: function() {
this.hideArrows();
// Position arrows always end at the node's margin box.
let marginBox = this.currentQuads.margin[0].bounds;
@@ -595,27 +665,32 @@ GeometryEditorHighlighter.prototype = ex
sideProp.cssRule.style.getPropertyValue(side));
}
},
updateArrow: function(side, mainStart, mainEnd, crossPos, labelValue) {
let arrowEl = this.getElement("arrow-" + side);
let labelEl = this.getElement("label-" + side);
let labelTextEl = this.getElement("label-text-" + side);
+ let handlerEl = this.getElement("handler-" + side);
// Position the arrow <line>.
arrowEl.setAttribute(GeoProp.axis(side) + "1", mainStart);
arrowEl.setAttribute(GeoProp.crossAxis(side) + "1", crossPos);
arrowEl.setAttribute(GeoProp.axis(side) + "2", mainEnd);
arrowEl.setAttribute(GeoProp.crossAxis(side) + "2", crossPos);
arrowEl.removeAttribute("hidden");
+ handlerEl.setAttribute("c" + GeoProp.axis(side), mainEnd);
+ handlerEl.setAttribute("c" + GeoProp.crossAxis(side), crossPos);
+ handlerEl.removeAttribute("hidden");
+
// Position the label <text> in the middle of the arrow (making sure it's
// not hidden below the fold).
- let capitalize = str => str.substring(0, 1).toUpperCase() + str.substring(1);
+ let capitalize = str => str[0].toUpperCase() + str.substring(1);
let winMain = this.win["inner" + capitalize(GeoProp.mainAxisSize(side))];
let labelMain = mainStart + (mainEnd - mainStart) / 2;
if ((mainStart > 0 && mainStart < winMain) ||
(mainEnd > 0 && mainEnd < winMain)) {
if (labelMain < GEOMETRY_LABEL_SIZE) {
labelMain = GEOMETRY_LABEL_SIZE;
} else if (labelMain > winMain - GEOMETRY_LABEL_SIZE) {
labelMain = winMain - GEOMETRY_LABEL_SIZE;
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -2,16 +2,17 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cu } = require("chrome");
const { getCurrentZoom,
getRootBindingParent } = require("devtools/shared/layout/utils");
+const { on, emit } = require("sdk/event/core");
const lazyContainer = {};
loader.lazyRequireGetter(lazyContainer, "CssLogic",
"devtools/shared/inspector/css-logic", true);
exports.getComputedStyle = (node) =>
lazyContainer.CssLogic.getComputedStyle(node);
@@ -32,16 +33,68 @@ exports.removePseudoClassLock = (...args
exports.getCSSStyleRules = (...args) =>
lazyContainer.DOMUtils.getCSSStyleRules(...args);
const SVG_NS = "http://www.w3.org/2000/svg";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const STYLESHEET_URI = "resource://devtools/server/actors/" +
"highlighters.css";
+const _tokens = Symbol("classList/tokens");
+
+/**
+ * Shims the element's `classList` for anonymous content elements; used
+ * internally by `CanvasFrameAnonymousContentHelper.getElement()` method.
+ */
+function ClassList(className) {
+ let trimmed = (className || "").trim();
+ this[_tokens] = trimmed ? trimmed.split(/\s+/) : [];
+}
+
+ClassList.prototype = {
+ item(index) {
+ return this[_tokens][index];
+ },
+ contains(token) {
+ return this[_tokens].includes(token);
+ },
+ add(token) {
+ if (!this.contains(token)) {
+ this[_tokens].push(token);
+ }
+ emit(this, "update");
+ },
+ remove(token) {
+ let index = this[_tokens].indexOf(token);
+
+ if (index > -1) {
+ this[_tokens].splice(index, 1);
+ }
+ emit(this, "update");
+ },
+ toggle(token) {
+ if (this.contains(token)) {
+ this.remove(token);
+ } else {
+ this.add(token);
+ }
+ },
+ get length() {
+ return this[_tokens].length;
+ },
+ [Symbol.iterator]: function* () {
+ for (let i = 0; i < this.tokens.length; i++) {
+ yield this[_tokens][i];
+ }
+ },
+ toString() {
+ return this[_tokens].join(" ");
+ }
+};
+
/**
* Is this content window a XUL window?
* @param {Window} window
* @return {Boolean}
*/
function isXUL(window) {
return window.document.documentElement.namespaceURI === XUL_NS;
}
@@ -271,16 +324,20 @@ CanvasFrameAnonymousContentHelper.protot
},
removeAttributeForElement: function(id, name) {
if (this.content) {
this.content.removeAttributeForElement(id, name);
}
},
+ hasAttributeForElement: function(id, name) {
+ return typeof this.getAttributeForElement(id, name) === "string";
+ },
+
/**
* Add an event listener to one of the elements inserted in the canvasFrame
* native anonymous container.
* Like other methods in this helper, this requires the ID of the element to
* be passed in.
*
* Note that if the content page navigates, the event listeners won't be
* added again.
@@ -393,29 +450,36 @@ CanvasFrameAnonymousContentHelper.protot
for (let [type] of this.listeners) {
target.removeEventListener(type, this, true);
}
}
this.listeners.clear();
},
getElement: function(id) {
- let self = this;
+ let classList = new ClassList(this.getAttributeForElement(id, "class"));
+
+ on(classList, "update", () => {
+ this.setAttributeForElement(id, "class", classList.toString());
+ });
+
return {
- getTextContent: () => self.getTextContentForElement(id),
- setTextContent: text => self.setTextContentForElement(id, text),
- setAttribute: (name, value) => self.setAttributeForElement(id, name, value),
- getAttribute: name => self.getAttributeForElement(id, name),
- removeAttribute: name => self.removeAttributeForElement(id, name),
+ getTextContent: () => this.getTextContentForElement(id),
+ setTextContent: text => this.setTextContentForElement(id, text),
+ setAttribute: (name, val) => this.setAttributeForElement(id, name, val),
+ getAttribute: name => this.getAttributeForElement(id, name),
+ removeAttribute: name => this.removeAttributeForElement(id, name),
+ hasAttribute: name => this.hasAttributeForElement(id, name),
addEventListener: (type, handler) => {
- return self.addEventListenerForElement(id, type, handler);
+ return this.addEventListenerForElement(id, type, handler);
},
removeEventListener: (type, handler) => {
- return self.removeEventListenerForElement(id, type, handler);
- }
+ return this.removeEventListenerForElement(id, type, handler);
+ },
+ classList
};
},
get content() {
if (!this._content || Cu.isDeadWrapper(this._content)) {
return null;
}
return this._content;
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -6,16 +6,19 @@
const {Cc, Ci} = require("chrome");
const promise = require("promise");
const protocol = require("devtools/server/protocol");
const {Arg, Option, method, RetVal, types} = protocol;
const events = require("sdk/event/core");
const {Class} = require("sdk/core/heritage");
const {LongStringActor} = require("devtools/server/actors/string");
+const {
+ getDefinedGeometryProperties
+} = require("devtools/server/actors/highlighters/geometry-editor");
// This will also add the "stylesheet" actor type for protocol.js to recognize
const {UPDATE_PRESERVING_RULES, UPDATE_GENERAL} =
require("devtools/server/actors/stylesheets");
loader.lazyRequireGetter(this, "CSS", "CSS");
loader.lazyGetter(this, "CssLogic", () => {
@@ -560,17 +563,17 @@ var PageStyleActor = protocol.ActorClass
* `filter`: A string filter that affects the "matched" handling.
* 'user': Include properties from user style sheets.
* 'ua': Include properties from user and user-agent sheets.
* Default value is 'ua'
* `inherited`: Include styles inherited from parent nodes.
* `matchedSelectors`: Include an array of specific selectors that
* caused this rule to match its node.
*/
- getApplied: method(Task.async(function*(node, options) {
+ getApplied: method(Task.async(function* (node, options) {
if (!node) {
return {entries: [], rules: [], sheets: []};
}
this.cssLogic.highlight(node.rawNode);
let entries = [];
entries = entries.concat(this._getAllElementRules(node, undefined,
options));
@@ -592,16 +595,34 @@ var PageStyleActor = protocol.ActorClass
}),
_hasInheritedProps: function(style) {
return Array.prototype.some.call(style, prop => {
return DOMUtils.isInheritedProperty(prop);
});
},
+ isPositionEditable: method(Task.async(function* (node) {
+ if (!node || node.rawNode.nodeType !== node.rawNode.ELEMENT_NODE) {
+ return false;
+ }
+
+ let props = getDefinedGeometryProperties(node.rawNode);
+
+ // Elements with only `width` and `height` are currently not considered
+ // editable.
+ return props.has("top") ||
+ props.has("right") ||
+ props.has("left") ||
+ props.has("bottom");
+ }), {
+ request: { node: Arg(0, "domnode")},
+ response: { value: RetVal("boolean") }
+ }),
+
/**
* Helper function for getApplied, gets all the rules from a given
* element. See getApplied for documentation on parameters.
* @param NodeActor node
* @param bool inherited
* @param object options
* @return Array The rules for a given element. Each item in the
@@ -964,17 +985,17 @@ var PageStyleActor = protocol.ActorClass
* @param {String} pseudoClasses The list of pseudo classes to append to the
* new selector.
* @param {Boolean} editAuthored
* True if the selector should be updated by editing the
* authored text; false if the selector should be updated via
* CSSOM.
* @returns {StyleRuleActor} the new rule
*/
- addNewRule: method(Task.async(function*(node, pseudoClasses,
+ addNewRule: method(Task.async(function* (node, pseudoClasses,
editAuthored = false) {
let style = this.styleElement;
let sheet = style.sheet;
let cssRules = sheet.cssRules;
let rawNode = node.rawNode;
let selector;
if (rawNode.id) {
@@ -1044,17 +1065,17 @@ protocol.FrontClass(PageStyleActor, {
getMatchedSelectors: protocol.custom(function(node, property, options) {
return this._getMatchedSelectors(node, property, options).then(ret => {
return ret.matched;
});
}, {
impl: "_getMatchedSelectors"
}),
- getApplied: protocol.custom(Task.async(function*(node, options = {}) {
+ getApplied: protocol.custom(Task.async(function* (node, options = {}) {
// If the getApplied method doesn't recreate the style cache itself, this
// means a call to cssLogic.highlight is required before trying to access
// the applied rules. Issue a request to getLayout if this is the case.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1103993#c16.
if (!this._form.traits || !this._form.traits.getAppliedCreatesStyleCache) {
yield this.getLayout(node);
}
let ret = yield this._getApplied(node, options);
@@ -1398,17 +1419,17 @@ var StyleRuleActor = protocol.ActorClass
/**
* Set the contents of the rule. This rewrites the rule in the
* stylesheet and causes it to be re-evaluated.
*
* @param {String} newText the new text of the rule
* @returns the rule with updated properties
*/
- setRuleText: method(Task.async(function*(newText) {
+ setRuleText: method(Task.async(function* (newText) {
if (!this.canSetRuleText ||
(this.type !== Ci.nsIDOMCSSRule.STYLE_RULE &&
this.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE)) {
throw new Error("invalid call to setRuleText");
}
let parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet);
let {str: cssText} = yield parentStyleSheet.getText();
@@ -1489,17 +1510,17 @@ var StyleRuleActor = protocol.ActorClass
* @param {Boolean} editAuthored
* True if the selector should be updated by editing the
* authored text; false if the selector should be updated via
* CSSOM.
*
* @returns {CSSRule}
* The new CSS rule added
*/
- _addNewSelector: Task.async(function*(value, editAuthored) {
+ _addNewSelector: Task.async(function* (value, editAuthored) {
let rule = this.rawRule;
let parentStyleSheet = this._parentSheet;
// We know the selector modification is ok, so if the client asked
// for the authored text to be edited, do it now.
if (editAuthored) {
let document = this.getDocument(this._parentSheet);
try {
@@ -1548,17 +1569,17 @@ var StyleRuleActor = protocol.ActorClass
* support was added in FF41.
*
* @param string value
* The new selector value
* @returns boolean
* Returns a boolean if the selector in the stylesheet was modified,
* and false otherwise
*/
- modifySelector: method(Task.async(function*(value) {
+ modifySelector: method(Task.async(function* (value) {
if (this.type === ELEMENT_STYLE) {
return false;
}
let document = this.getDocument(this._parentSheet);
// Extract the selector, and pseudo elements and classes
let [selector] = value.split(/(:{1,2}.+$)/);
let selectorElement;
@@ -1806,17 +1827,17 @@ protocol.FrontClass(StyleRuleActor, {
if (!source) {
location.href = this.href;
}
this._originalLocation = location;
return location;
});
},
- modifySelector: protocol.custom(Task.async(function*(node, value) {
+ modifySelector: protocol.custom(Task.async(function* (node, value) {
let response;
if (this.supportsModifySelectorUnmatched) {
// If the debugee supports adding unmatched rules (post FF41)
if (this.canSetRuleText) {
response = yield this.modifySelector2(node, value, true);
} else {
response = yield this.modifySelector2(node, value);
}