browser/devtools/layoutview/view.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 "use strict";
michael@0 8
michael@0 9 const Cu = Components.utils;
michael@0 10 const Ci = Components.interfaces;
michael@0 11 const Cc = Components.classes;
michael@0 12
michael@0 13 Cu.import("resource://gre/modules/Services.jsm");
michael@0 14 Cu.import("resource://gre/modules/Task.jsm");
michael@0 15 Cu.import("resource://gre/modules/devtools/Loader.jsm");
michael@0 16 Cu.import("resource://gre/modules/devtools/Console.jsm");
michael@0 17
michael@0 18 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 19 const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor");
michael@0 20 const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
michael@0 21
michael@0 22 const NUMERIC = /^-?[\d\.]+$/;
michael@0 23 const LONG_TEXT_ROTATE_LIMIT = 3;
michael@0 24
michael@0 25 /**
michael@0 26 * An instance of EditingSession tracks changes that have been made during the
michael@0 27 * modification of box model values. All of these changes can be reverted by
michael@0 28 * calling revert.
michael@0 29 *
michael@0 30 * @param doc A DOM document that can be used to test style rules.
michael@0 31 * @param rules An array of the style rules defined for the node being edited.
michael@0 32 * These should be in order of priority, least important first.
michael@0 33 */
michael@0 34 function EditingSession(doc, rules) {
michael@0 35 this._doc = doc;
michael@0 36 this._rules = rules;
michael@0 37 this._modifications = new Map();
michael@0 38 }
michael@0 39
michael@0 40 EditingSession.prototype = {
michael@0 41 /**
michael@0 42 * Gets the value of a single property from the CSS rule.
michael@0 43 *
michael@0 44 * @param rule The CSS rule
michael@0 45 * @param property The name of the property
michael@0 46 */
michael@0 47 getPropertyFromRule: function(rule, property) {
michael@0 48 let dummyStyle = this._element.style;
michael@0 49
michael@0 50 dummyStyle.cssText = rule.cssText;
michael@0 51 return dummyStyle.getPropertyValue(property);
michael@0 52 },
michael@0 53
michael@0 54 /**
michael@0 55 * Returns the current value for a property as a string or the empty string if
michael@0 56 * no style rules affect the property.
michael@0 57 *
michael@0 58 * @param property The name of the property as a string
michael@0 59 */
michael@0 60 getProperty: function(property) {
michael@0 61 // Create a hidden element for getPropertyFromRule to use
michael@0 62 let div = this._doc.createElement("div");
michael@0 63 div.setAttribute("style", "display: none");
michael@0 64 this._doc.body.appendChild(div);
michael@0 65 this._element = this._doc.createElement("p");
michael@0 66 div.appendChild(this._element);
michael@0 67
michael@0 68 // As the rules are in order of priority we can just iterate until we find
michael@0 69 // the first that defines a value for the property and return that.
michael@0 70 for (let rule of this._rules) {
michael@0 71 let value = this.getPropertyFromRule(rule, property);
michael@0 72 if (value !== "") {
michael@0 73 div.remove();
michael@0 74 return value;
michael@0 75 }
michael@0 76 }
michael@0 77 div.remove();
michael@0 78 return "";
michael@0 79 },
michael@0 80
michael@0 81 /**
michael@0 82 * Sets a number of properties on the node. Returns a promise that will be
michael@0 83 * resolved when the modifications are complete.
michael@0 84 *
michael@0 85 * @param properties An array of properties, each is an object with name and
michael@0 86 * value properties. If the value is "" then the property
michael@0 87 * is removed.
michael@0 88 */
michael@0 89 setProperties: function(properties) {
michael@0 90 let modifications = this._rules[0].startModifyingProperties();
michael@0 91
michael@0 92 for (let property of properties) {
michael@0 93 if (!this._modifications.has(property.name))
michael@0 94 this._modifications.set(property.name, this.getPropertyFromRule(this._rules[0], property.name));
michael@0 95
michael@0 96 if (property.value == "")
michael@0 97 modifications.removeProperty(property.name);
michael@0 98 else
michael@0 99 modifications.setProperty(property.name, property.value, "");
michael@0 100 }
michael@0 101
michael@0 102 return modifications.apply().then(null, console.error);
michael@0 103 },
michael@0 104
michael@0 105 /**
michael@0 106 * Reverts all of the property changes made by this instance. Returns a
michael@0 107 * promise that will be resolved when complete.
michael@0 108 */
michael@0 109 revert: function() {
michael@0 110 let modifications = this._rules[0].startModifyingProperties();
michael@0 111
michael@0 112 for (let [property, value] of this._modifications) {
michael@0 113 if (value != "")
michael@0 114 modifications.setProperty(property, value, "");
michael@0 115 else
michael@0 116 modifications.removeProperty(property);
michael@0 117 }
michael@0 118
michael@0 119 return modifications.apply().then(null, console.error);
michael@0 120 }
michael@0 121 };
michael@0 122
michael@0 123 function LayoutView(aInspector, aWindow)
michael@0 124 {
michael@0 125 this.inspector = aInspector;
michael@0 126
michael@0 127 // <browser> is not always available (for Chrome targets for example)
michael@0 128 if (this.inspector.target.tab) {
michael@0 129 this.browser = aInspector.target.tab.linkedBrowser;
michael@0 130 }
michael@0 131
michael@0 132 this.doc = aWindow.document;
michael@0 133 this.sizeLabel = this.doc.querySelector(".size > span");
michael@0 134 this.sizeHeadingLabel = this.doc.getElementById("element-size");
michael@0 135
michael@0 136 this.init();
michael@0 137 }
michael@0 138
michael@0 139 LayoutView.prototype = {
michael@0 140 init: function LV_init() {
michael@0 141 this.update = this.update.bind(this);
michael@0 142 this.onNewNode = this.onNewNode.bind(this);
michael@0 143 this.onNewSelection = this.onNewSelection.bind(this);
michael@0 144 this.inspector.selection.on("new-node-front", this.onNewSelection);
michael@0 145 this.inspector.sidebar.on("layoutview-selected", this.onNewNode);
michael@0 146
michael@0 147 // Store for the different dimensions of the node.
michael@0 148 // 'selector' refers to the element that holds the value in view.xhtml;
michael@0 149 // 'property' is what we are measuring;
michael@0 150 // 'value' is the computed dimension, computed in update().
michael@0 151 this.map = {
michael@0 152 position: {selector: "#element-position",
michael@0 153 property: "position",
michael@0 154 value: undefined},
michael@0 155 marginTop: {selector: ".margin.top > span",
michael@0 156 property: "margin-top",
michael@0 157 value: undefined},
michael@0 158 marginBottom: {selector: ".margin.bottom > span",
michael@0 159 property: "margin-bottom",
michael@0 160 value: undefined},
michael@0 161 // margin-left is a shorthand for some internal properties,
michael@0 162 // margin-left-ltr-source and margin-left-rtl-source for example. The
michael@0 163 // real margin value we want is in margin-left-value
michael@0 164 marginLeft: {selector: ".margin.left > span",
michael@0 165 property: "margin-left",
michael@0 166 realProperty: "margin-left-value",
michael@0 167 value: undefined},
michael@0 168 // margin-right behaves the same as margin-left
michael@0 169 marginRight: {selector: ".margin.right > span",
michael@0 170 property: "margin-right",
michael@0 171 realProperty: "margin-right-value",
michael@0 172 value: undefined},
michael@0 173 paddingTop: {selector: ".padding.top > span",
michael@0 174 property: "padding-top",
michael@0 175 value: undefined},
michael@0 176 paddingBottom: {selector: ".padding.bottom > span",
michael@0 177 property: "padding-bottom",
michael@0 178 value: undefined},
michael@0 179 // padding-left behaves the same as margin-left
michael@0 180 paddingLeft: {selector: ".padding.left > span",
michael@0 181 property: "padding-left",
michael@0 182 realProperty: "padding-left-value",
michael@0 183 value: undefined},
michael@0 184 // padding-right behaves the same as margin-left
michael@0 185 paddingRight: {selector: ".padding.right > span",
michael@0 186 property: "padding-right",
michael@0 187 realProperty: "padding-right-value",
michael@0 188 value: undefined},
michael@0 189 borderTop: {selector: ".border.top > span",
michael@0 190 property: "border-top-width",
michael@0 191 value: undefined},
michael@0 192 borderBottom: {selector: ".border.bottom > span",
michael@0 193 property: "border-bottom-width",
michael@0 194 value: undefined},
michael@0 195 borderLeft: {selector: ".border.left > span",
michael@0 196 property: "border-left-width",
michael@0 197 value: undefined},
michael@0 198 borderRight: {selector: ".border.right > span",
michael@0 199 property: "border-right-width",
michael@0 200 value: undefined},
michael@0 201 };
michael@0 202
michael@0 203 // Make each element the dimensions editable
michael@0 204 for (let i in this.map) {
michael@0 205 if (i == "position")
michael@0 206 continue;
michael@0 207
michael@0 208 let dimension = this.map[i];
michael@0 209 editableItem({ element: this.doc.querySelector(dimension.selector) }, (element, event) => {
michael@0 210 this.initEditor(element, event, dimension);
michael@0 211 });
michael@0 212 }
michael@0 213
michael@0 214 this.onNewNode();
michael@0 215 },
michael@0 216
michael@0 217 /**
michael@0 218 * Called when the user clicks on one of the editable values in the layoutview
michael@0 219 */
michael@0 220 initEditor: function LV_initEditor(element, event, dimension) {
michael@0 221 let { property, realProperty } = dimension;
michael@0 222 if (!realProperty)
michael@0 223 realProperty = property;
michael@0 224 let session = new EditingSession(document, this.elementRules);
michael@0 225 let initialValue = session.getProperty(realProperty);
michael@0 226
michael@0 227 let editor = new InplaceEditor({
michael@0 228 element: element,
michael@0 229 initial: initialValue,
michael@0 230
michael@0 231 start: (editor) => {
michael@0 232 editor.elt.parentNode.classList.add("editing");
michael@0 233 },
michael@0 234
michael@0 235 change: (value) => {
michael@0 236 if (NUMERIC.test(value))
michael@0 237 value += "px";
michael@0 238 let properties = [
michael@0 239 { name: property, value: value }
michael@0 240 ]
michael@0 241
michael@0 242 if (property.substring(0, 7) == "border-") {
michael@0 243 let bprop = property.substring(0, property.length - 5) + "style";
michael@0 244 let style = session.getProperty(bprop);
michael@0 245 if (!style || style == "none" || style == "hidden")
michael@0 246 properties.push({ name: bprop, value: "solid" });
michael@0 247 }
michael@0 248
michael@0 249 session.setProperties(properties);
michael@0 250 },
michael@0 251
michael@0 252 done: (value, commit) => {
michael@0 253 editor.elt.parentNode.classList.remove("editing");
michael@0 254 if (!commit)
michael@0 255 session.revert();
michael@0 256 }
michael@0 257 }, event);
michael@0 258 },
michael@0 259
michael@0 260 /**
michael@0 261 * Is the layoutview visible in the sidebar?
michael@0 262 */
michael@0 263 isActive: function LV_isActive() {
michael@0 264 return this.inspector.sidebar.getCurrentTabID() == "layoutview";
michael@0 265 },
michael@0 266
michael@0 267 /**
michael@0 268 * Destroy the nodes. Remove listeners.
michael@0 269 */
michael@0 270 destroy: function LV_destroy() {
michael@0 271 this.inspector.sidebar.off("layoutview-selected", this.onNewNode);
michael@0 272 this.inspector.selection.off("new-node-front", this.onNewSelection);
michael@0 273 if (this.browser) {
michael@0 274 this.browser.removeEventListener("MozAfterPaint", this.update, true);
michael@0 275 }
michael@0 276 this.sizeHeadingLabel = null;
michael@0 277 this.sizeLabel = null;
michael@0 278 this.inspector = null;
michael@0 279 this.doc = null;
michael@0 280 },
michael@0 281
michael@0 282 /**
michael@0 283 * Selection 'new-node-front' event handler.
michael@0 284 */
michael@0 285 onNewSelection: function() {
michael@0 286 let done = this.inspector.updating("layoutview");
michael@0 287 this.onNewNode().then(done, (err) => { console.error(err); done() });
michael@0 288 },
michael@0 289
michael@0 290 onNewNode: function LV_onNewNode() {
michael@0 291 if (this.isActive() &&
michael@0 292 this.inspector.selection.isConnected() &&
michael@0 293 this.inspector.selection.isElementNode()) {
michael@0 294 this.undim();
michael@0 295 } else {
michael@0 296 this.dim();
michael@0 297 }
michael@0 298 return this.update();
michael@0 299 },
michael@0 300
michael@0 301 /**
michael@0 302 * Hide the layout boxes. No node are selected.
michael@0 303 */
michael@0 304 dim: function LV_dim() {
michael@0 305 if (this.browser) {
michael@0 306 this.browser.removeEventListener("MozAfterPaint", this.update, true);
michael@0 307 }
michael@0 308 this.trackingPaint = false;
michael@0 309 this.doc.body.classList.add("dim");
michael@0 310 this.dimmed = true;
michael@0 311 },
michael@0 312
michael@0 313 /**
michael@0 314 * Show the layout boxes. A node is selected.
michael@0 315 */
michael@0 316 undim: function LV_undim() {
michael@0 317 if (!this.trackingPaint) {
michael@0 318 if (this.browser) {
michael@0 319 this.browser.addEventListener("MozAfterPaint", this.update, true);
michael@0 320 }
michael@0 321 this.trackingPaint = true;
michael@0 322 }
michael@0 323 this.doc.body.classList.remove("dim");
michael@0 324 this.dimmed = false;
michael@0 325 },
michael@0 326
michael@0 327 /**
michael@0 328 * Compute the dimensions of the node and update the values in
michael@0 329 * the layoutview/view.xhtml document. Returns a promise that will be resolved
michael@0 330 * when complete.
michael@0 331 */
michael@0 332 update: function LV_update() {
michael@0 333 let lastRequest = Task.spawn((function*() {
michael@0 334 if (!this.isActive() ||
michael@0 335 !this.inspector.selection.isConnected() ||
michael@0 336 !this.inspector.selection.isElementNode()) {
michael@0 337 return;
michael@0 338 }
michael@0 339
michael@0 340 let node = this.inspector.selection.nodeFront;
michael@0 341 let layout = yield this.inspector.pageStyle.getLayout(node, {
michael@0 342 autoMargins: !this.dimmed
michael@0 343 });
michael@0 344 let styleEntries = yield this.inspector.pageStyle.getApplied(node, {});
michael@0 345
michael@0 346 // If a subsequent request has been made, wait for that one instead.
michael@0 347 if (this._lastRequest != lastRequest) {
michael@0 348 return this._lastRequest;
michael@0 349 }
michael@0 350
michael@0 351 this._lastRequest = null;
michael@0 352 let width = layout.width;
michael@0 353 let height = layout.height;
michael@0 354 let newLabel = width + "x" + height;
michael@0 355 if (this.sizeHeadingLabel.textContent != newLabel) {
michael@0 356 this.sizeHeadingLabel.textContent = newLabel;
michael@0 357 }
michael@0 358
michael@0 359 // If the view is dimmed, no need to do anything more.
michael@0 360 if (this.dimmed) {
michael@0 361 this.inspector.emit("layoutview-updated");
michael@0 362 return null;
michael@0 363 }
michael@0 364
michael@0 365 for (let i in this.map) {
michael@0 366 let property = this.map[i].property;
michael@0 367 if (!(property in layout)) {
michael@0 368 // Depending on the actor version, some properties
michael@0 369 // might be missing.
michael@0 370 continue;
michael@0 371 }
michael@0 372 let parsedValue = parseInt(layout[property]);
michael@0 373 if (Number.isNaN(parsedValue)) {
michael@0 374 // Not a number. We use the raw string.
michael@0 375 // Useful for "position" for example.
michael@0 376 this.map[i].value = layout[property];
michael@0 377 } else {
michael@0 378 this.map[i].value = parsedValue;
michael@0 379 }
michael@0 380 }
michael@0 381
michael@0 382 let margins = layout.autoMargins;
michael@0 383 if ("top" in margins) this.map.marginTop.value = "auto";
michael@0 384 if ("right" in margins) this.map.marginRight.value = "auto";
michael@0 385 if ("bottom" in margins) this.map.marginBottom.value = "auto";
michael@0 386 if ("left" in margins) this.map.marginLeft.value = "auto";
michael@0 387
michael@0 388 for (let i in this.map) {
michael@0 389 let selector = this.map[i].selector;
michael@0 390 let span = this.doc.querySelector(selector);
michael@0 391 if (span.textContent.length > 0 &&
michael@0 392 span.textContent == this.map[i].value) {
michael@0 393 continue;
michael@0 394 }
michael@0 395 span.textContent = this.map[i].value;
michael@0 396 this.manageOverflowingText(span);
michael@0 397 }
michael@0 398
michael@0 399 width -= this.map.borderLeft.value + this.map.borderRight.value +
michael@0 400 this.map.paddingLeft.value + this.map.paddingRight.value;
michael@0 401
michael@0 402 height -= this.map.borderTop.value + this.map.borderBottom.value +
michael@0 403 this.map.paddingTop.value + this.map.paddingBottom.value;
michael@0 404
michael@0 405 let newValue = width + "x" + height;
michael@0 406 if (this.sizeLabel.textContent != newValue) {
michael@0 407 this.sizeLabel.textContent = newValue;
michael@0 408 }
michael@0 409
michael@0 410 this.elementRules = [e.rule for (e of styleEntries)];
michael@0 411
michael@0 412 this.inspector.emit("layoutview-updated");
michael@0 413 }).bind(this)).then(null, console.error);
michael@0 414
michael@0 415 return this._lastRequest = lastRequest;
michael@0 416 },
michael@0 417
michael@0 418 showBoxModel: function(options={}) {
michael@0 419 let toolbox = this.inspector.toolbox;
michael@0 420 let nodeFront = this.inspector.selection.nodeFront;
michael@0 421
michael@0 422 toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
michael@0 423 },
michael@0 424
michael@0 425 hideBoxModel: function() {
michael@0 426 let toolbox = this.inspector.toolbox;
michael@0 427
michael@0 428 toolbox.highlighterUtils.unhighlight();
michael@0 429 },
michael@0 430
michael@0 431 manageOverflowingText: function(span) {
michael@0 432 let classList = span.parentNode.classList;
michael@0 433
michael@0 434 if (classList.contains("left") || classList.contains("right")) {
michael@0 435 let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT;
michael@0 436 classList.toggle("rotate", force);
michael@0 437 }
michael@0 438 }
michael@0 439 };
michael@0 440
michael@0 441 let elts;
michael@0 442 let tooltip;
michael@0 443
michael@0 444 let onmouseover = function(e) {
michael@0 445 let region = e.target.getAttribute("data-box");
michael@0 446
michael@0 447 tooltip.textContent = e.target.getAttribute("tooltip");
michael@0 448 this.layoutview.showBoxModel({region: region});
michael@0 449
michael@0 450 return false;
michael@0 451 }.bind(window);
michael@0 452
michael@0 453 let onmouseout = function(e) {
michael@0 454 tooltip.textContent = "";
michael@0 455 this.layoutview.hideBoxModel();
michael@0 456
michael@0 457 return false;
michael@0 458 }.bind(window);
michael@0 459
michael@0 460 window.setPanel = function(panel) {
michael@0 461 this.layoutview = new LayoutView(panel, window);
michael@0 462
michael@0 463 // Tooltip mechanism
michael@0 464 elts = document.querySelectorAll("*[tooltip]");
michael@0 465 tooltip = document.querySelector(".tooltip");
michael@0 466 for (let i = 0; i < elts.length; i++) {
michael@0 467 let elt = elts[i];
michael@0 468 elt.addEventListener("mouseover", onmouseover, true);
michael@0 469 elt.addEventListener("mouseout", onmouseout, true);
michael@0 470 }
michael@0 471
michael@0 472 // Mark document as RTL or LTR:
michael@0 473 let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
michael@0 474 getService(Ci.nsIXULChromeRegistry);
michael@0 475 let dir = chromeReg.isLocaleRTL("global");
michael@0 476 document.body.setAttribute("dir", dir ? "rtl" : "ltr");
michael@0 477
michael@0 478 window.parent.postMessage("layoutview-ready", "*");
michael@0 479 };
michael@0 480
michael@0 481 window.onunload = function() {
michael@0 482 this.layoutview.destroy();
michael@0 483 if (elts) {
michael@0 484 for (let i = 0; i < elts.length; i++) {
michael@0 485 let elt = elts[i];
michael@0 486 elt.removeEventListener("mouseover", onmouseover, true);
michael@0 487 elt.removeEventListener("mouseout", onmouseout, true);
michael@0 488 }
michael@0 489 }
michael@0 490 };

mercurial