mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-07 20:17:37 +00:00
7e20285e70
The -*- file variable lines -*- establish per-file settings that Emacs will pick up. This patch makes the following changes to those lines (and touches nothing else): - Never set the buffer's mode. Years ago, Emacs did not have a good JavaScript mode, so it made sense to use Java or C++ mode in .js files. However, Emacs has had js-mode for years now; it's perfectly serviceable, and is available and enabled by default in all major Emacs packagings. Selecting a mode in the -*- file variable line -*- is almost always the wrong thing to do anyway. It overrides Emacs's default choice, which is (now) reasonable; and even worse, it overrides settings the user might have made in their '.emacs' file for that file extension. It's only useful when there's something specific about that particular file that makes a particular mode appropriate. - Correctly propagate settings that establish the correct indentation level for this file: c-basic-offset and js2-basic-offset should be js-indent-level. Whatever value they're given should be preserved; different parts of our tree use different indentation styles. - We don't use tabs in Mozilla JS code. Always set indent-tabs-mode: nil. Remove tab-width: settings, at least in files that don't contain tab characters. - Remove js2-mode settings that belong in the user's .emacs file, like js2-skip-preprocessor-directives.
560 lines
17 KiB
JavaScript
560 lines
17 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const Cu = Components.utils;
|
|
const Ci = Components.interfaces;
|
|
const Cc = Components.classes;
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
|
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
|
Cu.import("resource://gre/modules/devtools/Console.jsm");
|
|
|
|
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
|
const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
|
|
const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
|
|
const {ReflowFront} = devtools.require("devtools/server/actors/layout");
|
|
|
|
const NUMERIC = /^-?[\d\.]+$/;
|
|
const LONG_TEXT_ROTATE_LIMIT = 3;
|
|
|
|
/**
|
|
* An instance of EditingSession tracks changes that have been made during the
|
|
* modification of box model values. All of these changes can be reverted by
|
|
* calling revert.
|
|
*
|
|
* @param doc A DOM document that can be used to test style rules.
|
|
* @param rules An array of the style rules defined for the node being edited.
|
|
* These should be in order of priority, least important first.
|
|
*/
|
|
function EditingSession(doc, rules) {
|
|
this._doc = doc;
|
|
this._rules = rules;
|
|
this._modifications = new Map();
|
|
}
|
|
|
|
EditingSession.prototype = {
|
|
/**
|
|
* Gets the value of a single property from the CSS rule.
|
|
*
|
|
* @param rule The CSS rule
|
|
* @param property The name of the property
|
|
*/
|
|
getPropertyFromRule: function(rule, property) {
|
|
let dummyStyle = this._element.style;
|
|
|
|
dummyStyle.cssText = rule.cssText;
|
|
return dummyStyle.getPropertyValue(property);
|
|
},
|
|
|
|
/**
|
|
* Returns the current value for a property as a string or the empty string if
|
|
* no style rules affect the property.
|
|
*
|
|
* @param property The name of the property as a string
|
|
*/
|
|
getProperty: function(property) {
|
|
// Create a hidden element for getPropertyFromRule to use
|
|
let div = this._doc.createElement("div");
|
|
div.setAttribute("style", "display: none");
|
|
this._doc.body.appendChild(div);
|
|
this._element = this._doc.createElement("p");
|
|
div.appendChild(this._element);
|
|
|
|
// As the rules are in order of priority we can just iterate until we find
|
|
// the first that defines a value for the property and return that.
|
|
for (let rule of this._rules) {
|
|
let value = this.getPropertyFromRule(rule, property);
|
|
if (value !== "") {
|
|
div.remove();
|
|
return value;
|
|
}
|
|
}
|
|
div.remove();
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Sets a number of properties on the node. Returns a promise that will be
|
|
* resolved when the modifications are complete.
|
|
*
|
|
* @param properties An array of properties, each is an object with name and
|
|
* value properties. If the value is "" then the property
|
|
* is removed.
|
|
*/
|
|
setProperties: function(properties) {
|
|
let modifications = this._rules[0].startModifyingProperties();
|
|
|
|
for (let property of properties) {
|
|
if (!this._modifications.has(property.name)) {
|
|
this._modifications.set(property.name,
|
|
this.getPropertyFromRule(this._rules[0], property.name));
|
|
}
|
|
|
|
if (property.value == "") {
|
|
modifications.removeProperty(property.name);
|
|
} else {
|
|
modifications.setProperty(property.name, property.value, "");
|
|
}
|
|
}
|
|
|
|
return modifications.apply().then(null, console.error);
|
|
},
|
|
|
|
/**
|
|
* Reverts all of the property changes made by this instance. Returns a
|
|
* promise that will be resolved when complete.
|
|
*/
|
|
revert: function() {
|
|
let modifications = this._rules[0].startModifyingProperties();
|
|
|
|
for (let [property, value] of this._modifications) {
|
|
if (value != "") {
|
|
modifications.setProperty(property, value, "");
|
|
} else {
|
|
modifications.removeProperty(property);
|
|
}
|
|
}
|
|
|
|
return modifications.apply().then(null, console.error);
|
|
},
|
|
|
|
destroy: function() {
|
|
this._doc = null;
|
|
this._rules = null;
|
|
this._modifications.clear();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The layout-view panel
|
|
* @param {InspectorPanel} inspector An instance of the inspector-panel
|
|
* 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(".size > span");
|
|
this.sizeHeadingLabel = this.doc.getElementById("element-size");
|
|
|
|
this.init();
|
|
}
|
|
|
|
LayoutView.prototype = {
|
|
init: function() {
|
|
this.update = this.update.bind(this);
|
|
|
|
this.onNewSelection = this.onNewSelection.bind(this);
|
|
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);
|
|
|
|
// 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: {selector: "#element-position",
|
|
property: "position",
|
|
value: undefined},
|
|
marginTop: {selector: ".margin.top > span",
|
|
property: "margin-top",
|
|
value: undefined},
|
|
marginBottom: {selector: ".margin.bottom > span",
|
|
property: "margin-bottom",
|
|
value: undefined},
|
|
// margin-left is a shorthand for some internal properties,
|
|
// margin-left-ltr-source and margin-left-rtl-source for example. The
|
|
// real margin value we want is in margin-left-value
|
|
marginLeft: {selector: ".margin.left > span",
|
|
property: "margin-left",
|
|
realProperty: "margin-left-value",
|
|
value: undefined},
|
|
// margin-right behaves the same as margin-left
|
|
marginRight: {selector: ".margin.right > span",
|
|
property: "margin-right",
|
|
realProperty: "margin-right-value",
|
|
value: undefined},
|
|
paddingTop: {selector: ".padding.top > span",
|
|
property: "padding-top",
|
|
value: undefined},
|
|
paddingBottom: {selector: ".padding.bottom > span",
|
|
property: "padding-bottom",
|
|
value: undefined},
|
|
// padding-left behaves the same as margin-left
|
|
paddingLeft: {selector: ".padding.left > span",
|
|
property: "padding-left",
|
|
realProperty: "padding-left-value",
|
|
value: undefined},
|
|
// padding-right behaves the same as margin-left
|
|
paddingRight: {selector: ".padding.right > span",
|
|
property: "padding-right",
|
|
realProperty: "padding-right-value",
|
|
value: undefined},
|
|
borderTop: {selector: ".border.top > span",
|
|
property: "border-top-width",
|
|
value: undefined},
|
|
borderBottom: {selector: ".border.bottom > span",
|
|
property: "border-bottom-width",
|
|
value: undefined},
|
|
borderLeft: {selector: ".border.left > span",
|
|
property: "border-left-width",
|
|
value: undefined},
|
|
borderRight: {selector: ".border.right > span",
|
|
property: "border-right-width",
|
|
value: undefined},
|
|
};
|
|
|
|
// Make each element the dimensions editable
|
|
for (let i in this.map) {
|
|
if (i == "position")
|
|
continue;
|
|
|
|
let dimension = this.map[i];
|
|
editableItem({
|
|
element: this.doc.querySelector(dimension.selector)
|
|
}, (element, event) => {
|
|
this.initEditor(element, event, dimension);
|
|
});
|
|
}
|
|
|
|
this.onNewNode();
|
|
},
|
|
|
|
/**
|
|
* Start listening to reflows in the current tab.
|
|
*/
|
|
trackReflows: function() {
|
|
if (!this.reflowFront) {
|
|
let toolbox = this.inspector.toolbox;
|
|
if (toolbox.target.form.reflowActor) {
|
|
this.reflowFront = ReflowFront(toolbox.target.client, toolbox.target.form);
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.reflowFront.on("reflows", this.update);
|
|
this.reflowFront.start();
|
|
},
|
|
|
|
/**
|
|
* Stop listening to reflows in the current tab.
|
|
*/
|
|
untrackReflows: function() {
|
|
if (!this.reflowFront) {
|
|
return;
|
|
}
|
|
|
|
this.reflowFront.off("reflows", this.update);
|
|
this.reflowFront.stop();
|
|
},
|
|
|
|
/**
|
|
* Called when the user clicks on one of the editable values in the layoutview
|
|
*/
|
|
initEditor: function(element, event, dimension) {
|
|
let { property, realProperty } = dimension;
|
|
if (!realProperty)
|
|
realProperty = property;
|
|
let session = new EditingSession(document, this.elementRules);
|
|
let initialValue = session.getProperty(realProperty);
|
|
|
|
let editor = new InplaceEditor({
|
|
element: element,
|
|
initial: initialValue,
|
|
|
|
start: (editor) => {
|
|
editor.elt.parentNode.classList.add("editing");
|
|
},
|
|
|
|
change: (value) => {
|
|
if (NUMERIC.test(value)) {
|
|
value += "px";
|
|
}
|
|
|
|
let properties = [
|
|
{ name: property, value: value }
|
|
];
|
|
|
|
if (property.substring(0, 7) == "border-") {
|
|
let bprop = property.substring(0, property.length - 5) + "style";
|
|
let style = session.getProperty(bprop);
|
|
if (!style || style == "none" || style == "hidden") {
|
|
properties.push({ name: bprop, value: "solid" });
|
|
}
|
|
}
|
|
|
|
session.setProperties(properties);
|
|
},
|
|
|
|
done: (value, commit) => {
|
|
editor.elt.parentNode.classList.remove("editing");
|
|
if (!commit) {
|
|
session.revert();
|
|
session.destroy();
|
|
}
|
|
}
|
|
}, event);
|
|
},
|
|
|
|
/**
|
|
* Is the layoutview visible in the sidebar?
|
|
*/
|
|
isActive: function() {
|
|
return this.inspector &&
|
|
this.inspector.sidebar.getCurrentTabID() == "layoutview";
|
|
},
|
|
|
|
/**
|
|
* Destroy the nodes. Remove listeners.
|
|
*/
|
|
destroy: function() {
|
|
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.sizeHeadingLabel = null;
|
|
this.sizeLabel = null;
|
|
this.inspector = null;
|
|
this.doc = null;
|
|
|
|
if (this.reflowFront) {
|
|
this.untrackReflows();
|
|
this.reflowFront.destroy();
|
|
this.reflowFront = null;
|
|
}
|
|
},
|
|
|
|
onSidebarSelect: function(e, sidebar) {
|
|
if (sidebar !== "layoutview") {
|
|
this.dim();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Selection 'new-node-front' event handler.
|
|
*/
|
|
onNewSelection: function() {
|
|
let done = this.inspector.updating("layoutview");
|
|
this.onNewNode().then(done, (err) => { console.error(err); done() });
|
|
},
|
|
|
|
/**
|
|
* @return a promise that resolves when the view has been updated
|
|
*/
|
|
onNewNode: function() {
|
|
if (this.isActive() &&
|
|
this.inspector.selection.isConnected() &&
|
|
this.inspector.selection.isElementNode()) {
|
|
this.undim();
|
|
} else {
|
|
this.dim();
|
|
}
|
|
|
|
return this.update();
|
|
},
|
|
|
|
/**
|
|
* Hide the layout boxes and stop refreshing on reflows. No node is selected
|
|
* or the layout-view sidebar is inactive.
|
|
*/
|
|
dim: function() {
|
|
this.untrackReflows();
|
|
this.doc.body.classList.add("dim");
|
|
this.dimmed = true;
|
|
},
|
|
|
|
/**
|
|
* Show the layout boxes and start refreshing on reflows. A node is selected
|
|
* and the layout-view side is active.
|
|
*/
|
|
undim: function() {
|
|
this.trackReflows();
|
|
this.doc.body.classList.remove("dim");
|
|
this.dimmed = false;
|
|
},
|
|
|
|
/**
|
|
* 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*() {
|
|
if (!this.isActive() ||
|
|
!this.inspector.selection.isConnected() ||
|
|
!this.inspector.selection.isElementNode()) {
|
|
return;
|
|
}
|
|
|
|
let node = this.inspector.selection.nodeFront;
|
|
let layout = yield this.inspector.pageStyle.getLayout(node, {
|
|
autoMargins: !this.dimmed
|
|
});
|
|
let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
|
|
|
|
// 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;
|
|
let newLabel = width + "x" + height;
|
|
if (this.sizeHeadingLabel.textContent != newLabel) {
|
|
this.sizeHeadingLabel.textContent = newLabel;
|
|
}
|
|
|
|
// If the view is dimmed, no need to do anything more.
|
|
if (this.dimmed) {
|
|
this.inspector.emit("layoutview-updated");
|
|
return null;
|
|
}
|
|
|
|
for (let i in this.map) {
|
|
let property = this.map[i].property;
|
|
if (!(property in layout)) {
|
|
// Depending on the actor version, some properties
|
|
// might be missing.
|
|
continue;
|
|
}
|
|
let parsedValue = parseInt(layout[property]);
|
|
if (Number.isNaN(parsedValue)) {
|
|
// Not a number. We use the raw string.
|
|
// Useful for "position" for example.
|
|
this.map[i].value = layout[property];
|
|
} else {
|
|
this.map[i].value = parsedValue;
|
|
}
|
|
}
|
|
|
|
let margins = layout.autoMargins;
|
|
if ("top" in margins) this.map.marginTop.value = "auto";
|
|
if ("right" in margins) this.map.marginRight.value = "auto";
|
|
if ("bottom" in margins) this.map.marginBottom.value = "auto";
|
|
if ("left" in margins) this.map.marginLeft.value = "auto";
|
|
|
|
for (let i in this.map) {
|
|
let selector = this.map[i].selector;
|
|
let span = this.doc.querySelector(selector);
|
|
if (span.textContent.length > 0 &&
|
|
span.textContent == this.map[i].value) {
|
|
continue;
|
|
}
|
|
span.textContent = this.map[i].value;
|
|
this.manageOverflowingText(span);
|
|
}
|
|
|
|
width -= this.map.borderLeft.value + this.map.borderRight.value +
|
|
this.map.paddingLeft.value + this.map.paddingRight.value;
|
|
|
|
height -= this.map.borderTop.value + this.map.borderBottom.value +
|
|
this.map.paddingTop.value + this.map.paddingBottom.value;
|
|
|
|
let newValue = width + "x" + height;
|
|
if (this.sizeLabel.textContent != newValue) {
|
|
this.sizeLabel.textContent = newValue;
|
|
}
|
|
|
|
this.elementRules = [e.rule for (e of styleEntries)];
|
|
|
|
this.inspector.emit("layoutview-updated");
|
|
}).bind(this)).then(null, console.error);
|
|
|
|
return this._lastRequest = lastRequest;
|
|
},
|
|
|
|
/**
|
|
* Show the box-model highlighter on the currently selected element
|
|
* @param {Object} options Options passed to the highlighter actor
|
|
*/
|
|
showBoxModel: function(options={}) {
|
|
let toolbox = this.inspector.toolbox;
|
|
let nodeFront = this.inspector.selection.nodeFront;
|
|
|
|
toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
|
|
},
|
|
|
|
/**
|
|
* Hide the box-model highlighter on the currently selected element
|
|
*/
|
|
hideBoxModel: function() {
|
|
let toolbox = this.inspector.toolbox;
|
|
|
|
toolbox.highlighterUtils.unhighlight();
|
|
},
|
|
|
|
manageOverflowingText: function(span) {
|
|
let classList = span.parentNode.classList;
|
|
|
|
if (classList.contains("left") || classList.contains("right")) {
|
|
let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
|
|
classList.toggle("rotate", force);
|
|
}
|
|
}
|
|
};
|
|
|
|
let elts;
|
|
let tooltip;
|
|
|
|
let onmouseover = function(e) {
|
|
let region = e.target.getAttribute("data-box");
|
|
|
|
tooltip.textContent = e.target.getAttribute("tooltip");
|
|
this.layoutview.showBoxModel({region: region});
|
|
|
|
return false;
|
|
}.bind(window);
|
|
|
|
let onmouseout = function(e) {
|
|
tooltip.textContent = "";
|
|
this.layoutview.hideBoxModel();
|
|
|
|
return false;
|
|
}.bind(window);
|
|
|
|
window.setPanel = function(panel) {
|
|
this.layoutview = new LayoutView(panel, window);
|
|
|
|
// Tooltip mechanism
|
|
elts = document.querySelectorAll("*[tooltip]");
|
|
tooltip = document.querySelector(".tooltip");
|
|
for (let i = 0; i < elts.length; i++) {
|
|
let elt = elts[i];
|
|
elt.addEventListener("mouseover", onmouseover, true);
|
|
elt.addEventListener("mouseout", onmouseout, true);
|
|
}
|
|
|
|
// Mark document as RTL or LTR:
|
|
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
|
|
getService(Ci.nsIXULChromeRegistry);
|
|
let dir = chromeReg.isLocaleRTL("global");
|
|
document.body.setAttribute("dir", dir ? "rtl" : "ltr");
|
|
|
|
window.parent.postMessage("layoutview-ready", "*");
|
|
};
|
|
|
|
window.onunload = function() {
|
|
this.layoutview.destroy();
|
|
if (elts) {
|
|
for (let i = 0; i < elts.length; i++) {
|
|
let elt = elts[i];
|
|
elt.removeEventListener("mouseover", onmouseover, true);
|
|
elt.removeEventListener("mouseout", onmouseout, true);
|
|
}
|
|
}
|
|
};
|