Bug 1139187 - Allow moving and resizing elements in content; r=pbro

MozReview-Commit-ID: EmmFBXW22dk

--HG--
extra : rebase_source : 78e58f1a61dfafb61cf89498881c40d42c7a6c18
This commit is contained in:
Matteo Ferretti 2016-03-17 10:59:03 -04:00
Родитель de3f5b9261
Коммит 7d401d4ae4
15 изменённых файлов: 699 добавлений и 176 удалений

Просмотреть файл

@ -500,7 +500,7 @@ InspectorPanel.prototype = {
if (!this._updateProgress) {
// Start an update in progress.
var self = this;
let self = this;
this._updateProgress = {
node: this.selection.nodeFront,
outstanding: new Set(),
@ -508,7 +508,9 @@ InspectorPanel.prototype = {
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;
}

Просмотреть файл

@ -284,7 +284,11 @@
<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">

Просмотреть файл

@ -140,6 +140,7 @@ function LayoutView(inspector, win) {
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();
}
@ -157,6 +158,11 @@ LayoutView.prototype = {
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.
@ -253,6 +259,11 @@ LayoutView.prototype = {
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() {
@ -376,9 +387,23 @@ LayoutView.prototype = {
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;
@ -401,10 +426,12 @@ LayoutView.prototype = {
*/
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);
},
/**
@ -432,6 +459,33 @@ LayoutView.prototype = {
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.
@ -459,7 +513,7 @@ LayoutView.prototype = {
* @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;
}
@ -470,6 +524,8 @@ LayoutView.prototype = {
});
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;
@ -548,7 +604,7 @@ LayoutView.prototype = {
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;
@ -608,6 +664,77 @@ LayoutView.prototype = {
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;

Просмотреть файл

@ -209,6 +209,8 @@ MarkupView.prototype = {
}
}
this._showContainerAsHovered(container.node);
this.emit("node-hover");
},
/**
@ -341,6 +343,8 @@ MarkupView.prototype = {
this.getContainer(this._hoveredNode).hovered = false;
}
this._hoveredNode = null;
this.emit("leave");
},
/**

Просмотреть файл

@ -57,6 +57,7 @@ support-files =
[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]

Просмотреть файл

@ -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"))
};
}

Просмотреть файл

@ -222,6 +222,7 @@ devtools.jar:
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)

Просмотреть файл

@ -16,8 +16,13 @@
- 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">

Просмотреть файл

@ -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>

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Просмотреть файл

@ -336,3 +336,16 @@
#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;
}

Просмотреть файл

@ -29,6 +29,10 @@
display: none;
}
:-moz-native-anonymous .highlighter-container [dragging] {
cursor: grabbing;
}
/* Box model highlighter */
:-moz-native-anonymous .box-model-regions {
@ -207,6 +211,7 @@
/* 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 {
@ -228,6 +233,35 @@
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;

Просмотреть файл

@ -462,6 +462,8 @@ var CustomHighlighterActor = exports.CustomHighlighterActor = protocol.ActorClas
this._inspector = null;
},
release: method(function() {}, { release: true }),
/**
* Show the highlighter.
* This calls through to the highlighter instance's |show(node, options)|

Просмотреть файл

@ -15,6 +15,12 @@ const { setIgnoreLayoutChanges,
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.
@ -112,6 +118,75 @@ function getOffsetParent(node) {
};
}
/**
* 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.
@ -138,6 +213,20 @@ function GeometryEditorHighlighter(highlighterEnv) {
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, {
@ -154,7 +243,8 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
parent: container,
attributes: {
"id": "root",
"class": "root"
"class": "root",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
@ -194,7 +284,7 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
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",
@ -207,6 +297,19 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
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
@ -256,51 +359,21 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
});
}
// 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();
@ -309,72 +382,96 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
this.offsetParent = null;
},
getElement: function(id) {
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
handleEvent: function(event, id) {
// No event handling if the highlighter is hidden
if (this.getElement("root").hasAttribute("hidden")) {
return;
}
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
};
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;
}
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;
// 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;
}
},
/**
* 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
});
}
}
}
// 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
});
}
}
// 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;
// Top/left/bottom/right on static positioned elements have no effect.
if (pos === "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 (pos === "relative" && (hasRightAndLeft || hasBottomAndTop)) {
props.delete(name);
}
}
return props;
getElement: function(id) {
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
},
_show: function() {
@ -391,13 +488,16 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
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");
@ -410,12 +510,12 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
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;
},
@ -488,52 +588,22 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
_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");
}
},
@ -600,6 +670,7 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
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);
@ -608,9 +679,13 @@ GeometryEditorHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
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) ||

Просмотреть файл

@ -7,6 +7,7 @@
const { Cc, Ci, Cu } = require("chrome");
const { getCurrentZoom,
getRootBindingParent } = require("devtools/shared/layout/utils");
const { on, emit } = require("sdk/event/core");
const lazyContainer = {};
@ -37,6 +38,58 @@ 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
@ -276,6 +329,10 @@ CanvasFrameAnonymousContentHelper.prototype = {
}
},
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.
@ -398,19 +455,26 @@ CanvasFrameAnonymousContentHelper.prototype = {
},
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
};
},

Просмотреть файл

@ -11,6 +11,9 @@ 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} =
@ -565,7 +568,7 @@ var PageStyleActor = protocol.ActorClass({
* `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: []};
}
@ -597,6 +600,24 @@ var PageStyleActor = protocol.ActorClass({
});
},
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.
@ -969,7 +990,7 @@ var PageStyleActor = protocol.ActorClass({
* 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;
@ -1049,7 +1070,7 @@ protocol.FrontClass(PageStyleActor, {
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.
@ -1403,7 +1424,7 @@ var StyleRuleActor = protocol.ActorClass({
* @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)) {
@ -1494,7 +1515,7 @@ var StyleRuleActor = protocol.ActorClass({
* @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;
@ -1553,7 +1574,7 @@ var StyleRuleActor = protocol.ActorClass({
* 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;
}
@ -1811,7 +1832,7 @@ protocol.FrontClass(StyleRuleActor, {
});
},
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)