summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/components/box-model.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/inspector/components/box-model.js')
-rw-r--r--devtools/client/inspector/components/box-model.js841
1 files changed, 841 insertions, 0 deletions
diff --git a/devtools/client/inspector/components/box-model.js b/devtools/client/inspector/components/box-model.js
new file mode 100644
index 000000000..fc36fac71
--- /dev/null
+++ b/devtools/client/inspector/components/box-model.js
@@ -0,0 +1,841 @@
+/* -*- 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 {Task} = require("devtools/shared/task");
+const {InplaceEditor, editableItem} =
+ require("devtools/client/shared/inplace-editor");
+const {ReflowFront} = require("devtools/shared/fronts/reflow");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+const STRINGS_URI = "devtools/client/locales/shared.properties";
+const STRINGS_INSPECTOR = "devtools/shared/locales/styleinspector.properties";
+const SHARED_L10N = new LocalizationHelper(STRINGS_URI);
+const INSPECTOR_L10N = new LocalizationHelper(STRINGS_INSPECTOR);
+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. The main parameter is the BoxModelView that created it.
+ *
+ * @param inspector The inspector panel.
+ * @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({inspector, doc, elementRules}) {
+ this._doc = doc;
+ this._rules = elementRules;
+ this._modifications = new Map();
+ this._cssProperties = getCssProperties(inspector.toolbox);
+}
+
+EditingSession.prototype = {
+ /**
+ * Gets the value of a single property from the CSS rule.
+ *
+ * @param {StyleRuleFront} rule The CSS rule.
+ * @param {String} property The name of the property.
+ * @return {String} The value.
+ */
+ getPropertyFromRule: function (rule, property) {
+ // Use the parsed declarations in the StyleRuleFront object if available.
+ let index = this.getPropertyIndex(property, rule);
+ if (index !== -1) {
+ return rule.declarations[index].value;
+ }
+
+ // Fallback to parsing the cssText locally otherwise.
+ 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.getElementById("sidebar-panel-computedview").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 "";
+ },
+
+ /**
+ * Get the index of a given css property name in a CSS rule.
+ * Or -1, if there are no properties in the rule yet.
+ * @param {String} name The property name.
+ * @param {StyleRuleFront} rule Optional, defaults to the element style rule.
+ * @return {Number} The property index in the rule.
+ */
+ getPropertyIndex: function (name, rule = this._rules[0]) {
+ let elementStyleRule = this._rules[0];
+ if (!elementStyleRule.declarations.length) {
+ return -1;
+ }
+
+ return elementStyleRule.declarations.findIndex(p => p.name === name);
+ },
+
+ /**
+ * Sets a number of properties on the node.
+ * @param properties An array of properties, each is an object with name and
+ * value properties. If the value is "" then the property
+ * is removed.
+ * @return {Promise} Resolves when the modifications are complete.
+ */
+ setProperties: Task.async(function* (properties) {
+ for (let property of properties) {
+ // Get a RuleModificationList or RuleRewriter helper object from the
+ // StyleRuleActor to make changes to CSS properties.
+ // Note that RuleRewriter doesn't support modifying several properties at
+ // once, so we do this in a sequence here.
+ let modifications = this._rules[0].startModifyingProperties(
+ this._cssProperties);
+
+ // Remember the property so it can be reverted.
+ if (!this._modifications.has(property.name)) {
+ this._modifications.set(property.name,
+ this.getPropertyFromRule(this._rules[0], property.name));
+ }
+
+ // Find the index of the property to be changed, or get the next index to
+ // insert the new property at.
+ let index = this.getPropertyIndex(property.name);
+ if (index === -1) {
+ index = this._rules[0].declarations.length;
+ }
+
+ if (property.value == "") {
+ modifications.removeProperty(index, property.name);
+ } else {
+ modifications.setProperty(index, property.name, property.value, "");
+ }
+
+ yield modifications.apply();
+ }
+ }),
+
+ /**
+ * Reverts all of the property changes made by this instance.
+ * @return {Promise} Resolves when all properties have been reverted.
+ */
+ revert: Task.async(function* () {
+ // Revert each property that we modified previously, one by one. See
+ // setProperties for information about why.
+ for (let [property, value] of this._modifications) {
+ let modifications = this._rules[0].startModifyingProperties(
+ this._cssProperties);
+
+ // Find the index of the property to be reverted.
+ let index = this.getPropertyIndex(property);
+
+ if (value != "") {
+ // If the property doesn't exist anymore, insert at the beginning of the
+ // rule.
+ if (index === -1) {
+ index = 0;
+ }
+ modifications.setProperty(index, property, value, "");
+ } else {
+ // If the property doesn't exist anymore, no need to remove it. It had
+ // not been added after all.
+ if (index === -1) {
+ continue;
+ }
+ modifications.removeProperty(index, property);
+ }
+
+ yield modifications.apply();
+ }
+ }),
+
+ destroy: function () {
+ this._doc = null;
+ this._rules = null;
+ this._modifications.clear();
+ }
+};
+
+/**
+ * The box model view
+ * @param {InspectorPanel} inspector
+ * An instance of the inspector-panel currently loaded in the toolbox
+ * @param {Document} document
+ * The document that will contain the box model view.
+ */
+function BoxModelView(inspector, document) {
+ this.inspector = inspector;
+ this.doc = document;
+ this.wrapper = this.doc.getElementById("boxmodel-wrapper");
+ this.container = this.doc.getElementById("boxmodel-container");
+ this.expander = this.doc.getElementById("boxmodel-expander");
+ this.sizeLabel = this.doc.querySelector(".boxmodel-size > span");
+ this.sizeHeadingLabel = this.doc.getElementById("boxmodel-element-size");
+ this._geometryEditorHighlighter = null;
+ this._cssProperties = getCssProperties(inspector.toolbox);
+
+ this.init();
+}
+
+BoxModelView.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("computedview-selected", this.onNewNode);
+
+ this.onSidebarSelect = this.onSidebarSelect.bind(this);
+ this.inspector.sidebar.on("select", this.onSidebarSelect);
+
+ this.onToggleExpander = this.onToggleExpander.bind(this);
+ this.expander.addEventListener("click", this.onToggleExpander);
+ let header = this.doc.getElementById("boxmodel-header");
+ header.addEventListener("dblclick", this.onToggleExpander);
+
+ this.onFilterComputedView = this.onFilterComputedView.bind(this);
+ this.inspector.on("computed-view-filtered",
+ this.onFilterComputedView);
+
+ this.onPickerStarted = this.onPickerStarted.bind(this);
+ this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
+ this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
+ this.onWillNavigate = this.onWillNavigate.bind(this);
+
+ this.initBoxModelHighlighter();
+
+ // Store for the different dimensions of the node.
+ // 'selector' refers to the element that holds the value;
+ // 'property' is what we are measuring;
+ // 'value' is the computed dimension, computed in update().
+ this.map = {
+ position: {
+ selector: "#boxmodel-element-position",
+ property: "position",
+ value: undefined
+ },
+ marginTop: {
+ selector: ".boxmodel-margin.boxmodel-top > span",
+ property: "margin-top",
+ value: undefined
+ },
+ marginBottom: {
+ selector: ".boxmodel-margin.boxmodel-bottom > span",
+ property: "margin-bottom",
+ value: undefined
+ },
+ marginLeft: {
+ selector: ".boxmodel-margin.boxmodel-left > span",
+ property: "margin-left",
+ value: undefined
+ },
+ marginRight: {
+ selector: ".boxmodel-margin.boxmodel-right > span",
+ property: "margin-right",
+ value: undefined
+ },
+ paddingTop: {
+ selector: ".boxmodel-padding.boxmodel-top > span",
+ property: "padding-top",
+ value: undefined
+ },
+ paddingBottom: {
+ selector: ".boxmodel-padding.boxmodel-bottom > span",
+ property: "padding-bottom",
+ value: undefined
+ },
+ paddingLeft: {
+ selector: ".boxmodel-padding.boxmodel-left > span",
+ property: "padding-left",
+ value: undefined
+ },
+ paddingRight: {
+ selector: ".boxmodel-padding.boxmodel-right > span",
+ property: "padding-right",
+ value: undefined
+ },
+ borderTop: {
+ selector: ".boxmodel-border.boxmodel-top > span",
+ property: "border-top-width",
+ value: undefined
+ },
+ borderBottom: {
+ selector: ".boxmodel-border.boxmodel-bottom > span",
+ property: "border-bottom-width",
+ value: undefined
+ },
+ borderLeft: {
+ selector: ".boxmodel-border.boxmodel-left > span",
+ property: "border-left-width",
+ value: undefined
+ },
+ borderRight: {
+ selector: ".boxmodel-border.boxmodel-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();
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ this.onGeometryButtonClick = this.onGeometryButtonClick.bind(this);
+ nodeGeometry.addEventListener("click", this.onGeometryButtonClick);
+ },
+
+ initBoxModelHighlighter: function () {
+ let highlightElts = this.doc.querySelectorAll("#boxmodel-container *[title]");
+ this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
+ this.onHighlightMouseOut = this.onHighlightMouseOut.bind(this);
+
+ for (let element of highlightElts) {
+ element.addEventListener("mouseover", this.onHighlightMouseOver, true);
+ element.addEventListener("mouseout", this.onHighlightMouseOut, true);
+ }
+ },
+
+ /**
+ * Start listening to reflows in the current tab.
+ */
+ trackReflows: function () {
+ if (!this.reflowFront) {
+ let { target } = this.inspector;
+ if (target.form.reflowActor) {
+ this.reflowFront = ReflowFront(target.client,
+ 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 box model view
+ */
+ initEditor: function (element, event, dimension) {
+ let { property } = dimension;
+ let session = new EditingSession(this);
+ let initialValue = session.getProperty(property);
+
+ let editor = new InplaceEditor({
+ element: element,
+ initial: initialValue,
+ contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
+ property: {
+ name: dimension.property
+ },
+ start: self => {
+ self.elt.parentNode.classList.add("boxmodel-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).catch(e => console.error(e));
+ },
+ done: (value, commit) => {
+ editor.elt.parentNode.classList.remove("boxmodel-editing");
+ if (!commit) {
+ session.revert().then(() => {
+ session.destroy();
+ }, e => console.error(e));
+ }
+ },
+ contextMenu: this.inspector.onTextBoxContextMenu,
+ cssProperties: this._cssProperties
+ }, event);
+ },
+
+ /**
+ * Is the BoxModelView visible in the sidebar.
+ * @return {Boolean}
+ */
+ isViewVisible: function () {
+ return this.inspector &&
+ this.inspector.sidebar.getCurrentTabID() == "computedview";
+ },
+
+ /**
+ * Is the BoxModelView visible in the sidebar and is the current node valid to
+ * be displayed in the view.
+ * @return {Boolean}
+ */
+ isViewVisibleAndNodeValid: function () {
+ return this.isViewVisible() &&
+ this.inspector.selection.isConnected() &&
+ this.inspector.selection.isElementNode();
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners.
+ */
+ destroy: function () {
+ let highlightElts = this.doc.querySelectorAll("#boxmodel-container *[title]");
+
+ for (let element of highlightElts) {
+ element.removeEventListener("mouseover", this.onHighlightMouseOver, true);
+ element.removeEventListener("mouseout", this.onHighlightMouseOut, true);
+ }
+
+ this.expander.removeEventListener("click", this.onToggleExpander);
+ let header = this.doc.getElementById("boxmodel-header");
+ header.removeEventListener("dblclick", this.onToggleExpander);
+
+ 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 BoxModelView
+ // 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("computedview-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.inspector.off("computed-view-filtered", this.onFilterComputedView);
+
+ this.inspector = null;
+ this.doc = null;
+ this.wrapper = null;
+ this.container = null;
+ this.expander = null;
+ this.sizeLabel = null;
+ this.sizeHeadingLabel = null;
+
+ if (this.reflowFront) {
+ this.untrackReflows();
+ this.reflowFront.destroy();
+ this.reflowFront = null;
+ }
+ },
+
+ onSidebarSelect: function (e, sidebar) {
+ this.setActive(sidebar === "computedview");
+ },
+
+ /**
+ * Selection 'new-node-front' event handler.
+ */
+ onNewSelection: function () {
+ let done = this.inspector.updating("computed-view");
+ this.onNewNode()
+ .then(() => this.hideGeometryEditor())
+ .then(done, (err) => {
+ console.error(err);
+ done();
+ }).catch(console.error);
+ },
+
+ /**
+ * @return a promise that resolves when the view has been updated
+ */
+ onNewNode: function () {
+ this.setActive(this.isViewVisibleAndNodeValid());
+ return this.update();
+ },
+
+ onHighlightMouseOver: function (e) {
+ let region = e.target.getAttribute("data-box");
+ if (!region) {
+ return;
+ }
+
+ this.showBoxModel({
+ region,
+ showOnly: region,
+ onlyRegionArea: true
+ });
+ },
+
+ onHighlightMouseOut: function () {
+ this.hideBoxModel();
+ },
+
+ onGeometryButtonClick: function ({target}) {
+ if (target.hasAttribute("checked")) {
+ target.removeAttribute("checked");
+ this.hideGeometryEditor();
+ } else {
+ target.setAttribute("checked", "true");
+ this.showGeometryEditor();
+ }
+ },
+
+ onPickerStarted: function () {
+ this.hideGeometryEditor();
+ },
+
+ onToggleExpander: function () {
+ let isOpen = this.expander.hasAttribute("open");
+
+ if (isOpen) {
+ this.container.hidden = true;
+ this.expander.removeAttribute("open");
+ } else {
+ this.container.hidden = false;
+ this.expander.setAttribute("open", "");
+ }
+ },
+
+ onMarkupViewLeave: function () {
+ this.showGeometryEditor(true);
+ },
+
+ onMarkupViewNodeHover: function () {
+ this.hideGeometryEditor(false);
+ },
+
+ onWillNavigate: function () {
+ this._geometryEditorHighlighter.release().catch(console.error);
+ this._geometryEditorHighlighter = null;
+ },
+
+ /**
+ * Event handler that responds to the computed view being filtered
+ * @param {String} reason
+ * @param {Boolean} hidden
+ * Whether or not to hide the box model wrapper
+ */
+ onFilterComputedView: function (reason, hidden) {
+ this.wrapper.hidden = hidden;
+ },
+
+ /**
+ * Stop tracking reflows and hide all values when no node is selected or the
+ * box model view is hidden, otherwise track reflows and show values.
+ * @param {Boolean} isActive
+ */
+ setActive: function (isActive) {
+ if (isActive === this.isActive) {
+ return;
+ }
+ this.isActive = isActive;
+
+ if (isActive) {
+ this.trackReflows();
+ } else {
+ this.untrackReflows();
+ }
+ },
+
+ /**
+ * Compute the dimensions of the node and update the values in
+ * the inspector.xul document.
+ * @return a promise that will be resolved when complete.
+ */
+ update: function () {
+ let lastRequest = Task.spawn((function* () {
+ if (!this.isViewVisibleAndNodeValid()) {
+ this.wrapper.hidden = true;
+ this.inspector.emit("boxmodel-view-updated");
+ return null;
+ }
+
+ let node = this.inspector.selection.nodeFront;
+ let layout = yield this.inspector.pageStyle.getLayout(node, {
+ autoMargins: this.isActive
+ });
+ let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
+
+ yield this.updateGeometryButton();
+
+ // If a subsequent request has been made, wait for that one instead.
+ if (this._lastRequest != lastRequest) {
+ return this._lastRequest;
+ }
+
+ this._lastRequest = null;
+ let width = layout.width;
+ let height = layout.height;
+ let newLabel = SHARED_L10N.getFormatStr("dimensions", width, height);
+
+ if (this.sizeHeadingLabel.textContent != newLabel) {
+ this.sizeHeadingLabel.textContent = newLabel;
+ }
+
+ 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 = parseFloat(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);
+ this.updateSourceRuleTooltip(span, this.map[i].property, styleEntries);
+ 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;
+ width = parseFloat(width.toPrecision(6));
+ height -= this.map.borderTop.value + this.map.borderBottom.value +
+ this.map.paddingTop.value + this.map.paddingBottom.value;
+ height = parseFloat(height.toPrecision(6));
+
+ let newValue = width + "\u00D7" + height;
+ if (this.sizeLabel.textContent != newValue) {
+ this.sizeLabel.textContent = newValue;
+ }
+
+ this.elementRules = styleEntries.map(e => e.rule);
+
+ this.wrapper.hidden = false;
+
+ this.inspector.emit("boxmodel-view-updated");
+ return null;
+ }).bind(this)).catch(console.error);
+
+ this._lastRequest = lastRequest;
+ return this._lastRequest;
+ },
+
+ /**
+ * Update the text in the tooltip shown when hovering over a value to provide
+ * information about the source CSS rule that sets this value.
+ * @param {DOMNode} el The element that will receive the tooltip.
+ * @param {String} property The name of the CSS property for the tooltip.
+ * @param {Array} rules An array of applied rules retrieved by
+ * styleActor.getApplied.
+ */
+ updateSourceRuleTooltip: function (el, property, rules) {
+ // Dummy element used to parse the cssText of applied rules.
+ let dummyEl = this.doc.createElement("div");
+
+ // Rules are in order of priority so iterate until we find the first that
+ // defines a value for the property.
+ let sourceRule, value;
+ for (let {rule} of rules) {
+ dummyEl.style.cssText = rule.cssText;
+ value = dummyEl.style.getPropertyValue(property);
+ if (value !== "") {
+ sourceRule = rule;
+ break;
+ }
+ }
+
+ let title = property;
+ if (sourceRule && sourceRule.selectors) {
+ title += "\n" + sourceRule.selectors.join(", ");
+ }
+ if (sourceRule && sourceRule.parentStyleSheet) {
+ if (sourceRule.parentStyleSheet.href) {
+ title += "\n" + sourceRule.parentStyleSheet.href + ":" + sourceRule.line;
+ } else {
+ title += "\n" + INSPECTOR_L10N.getStr("rule.sourceInline") +
+ ":" + sourceRule.line;
+ }
+ }
+
+ el.setAttribute("title", title);
+ },
+
+ /**
+ * 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();
+ },
+
+ /**
+ * Show the geometry editor highlighter on the currently selected element
+ * @param {Boolean} [showOnlyIfActive=false]
+ * Indicates if the Geometry Editor should be shown only if it's active but
+ * hidden.
+ */
+ showGeometryEditor: function (showOnlyIfActive = false) {
+ let toolbox = this.inspector.toolbox;
+ let nodeFront = this.inspector.selection.nodeFront;
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ let isActive = nodeGeometry.hasAttribute("checked");
+
+ if (showOnlyIfActive && !isActive) {
+ return;
+ }
+
+ if (this._geometryEditorHighlighter) {
+ this._geometryEditorHighlighter.show(nodeFront).catch(console.error);
+ return;
+ }
+
+ // instantiate Geometry Editor highlighter
+ toolbox.highlighterUtils
+ .getHighlighterByType("GeometryEditorHighlighter").then(highlighter => {
+ highlighter.show(nodeFront).catch(console.error);
+ this._geometryEditorHighlighter = highlighter;
+
+ // Hide completely the geometry editor if the picker is clicked
+ toolbox.on("picker-started", this.onPickerStarted);
+
+ // Temporary hide the geometry editor
+ this.inspector.markup.on("leave", this.onMarkupViewLeave);
+ this.inspector.markup.on("node-hover", this.onMarkupViewNodeHover);
+
+ // Release the actor on will-navigate event
+ this.inspector.target.once("will-navigate", this.onWillNavigate);
+ });
+ },
+
+ /**
+ * Hide the geometry editor highlighter on the currently selected element
+ * @param {Boolean} [updateButton=true]
+ * Indicates if the Geometry Editor's button needs to be unchecked too
+ */
+ hideGeometryEditor: function (updateButton = true) {
+ if (this._geometryEditorHighlighter) {
+ this._geometryEditorHighlighter.hide().catch(console.error);
+ }
+
+ if (updateButton) {
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Update the visibility and the state of the geometry editor button,
+ * based on the selected node.
+ */
+ updateGeometryButton: Task.async(function* () {
+ let node = this.inspector.selection.nodeFront;
+ let isEditable = false;
+
+ if (node) {
+ isEditable = yield this.inspector.pageStyle.isPositionEditable(node);
+ }
+
+ let nodeGeometry = this.doc.getElementById("layout-geometry-editor");
+ nodeGeometry.style.visibility = isEditable ? "visible" : "hidden";
+ }),
+
+ manageOverflowingText: function (span) {
+ let classList = span.parentNode.classList;
+
+ if (classList.contains("boxmodel-left") ||
+ classList.contains("boxmodel-right")) {
+ let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
+ classList.toggle("boxmodel-rotate", force);
+ }
+ }
+};
+
+exports.BoxModelView = BoxModelView;