зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
de3f5b9261
Коммит
7d401d4ae4
|
@ -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)
|
||||
|
|
Загрузка…
Ссылка в новой задаче