browser/devtools/layoutview/view.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial