1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/layoutview/view.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,490 @@ 1.4 +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +const Cu = Components.utils; 1.13 +const Ci = Components.interfaces; 1.14 +const Cc = Components.classes; 1.15 + 1.16 +Cu.import("resource://gre/modules/Services.jsm"); 1.17 +Cu.import("resource://gre/modules/Task.jsm"); 1.18 +Cu.import("resource://gre/modules/devtools/Loader.jsm"); 1.19 +Cu.import("resource://gre/modules/devtools/Console.jsm"); 1.20 + 1.21 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.22 +const {InplaceEditor, editableItem} = devtools.require("devtools/shared/inplace-editor"); 1.23 +const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils"); 1.24 + 1.25 +const NUMERIC = /^-?[\d\.]+$/; 1.26 +const LONG_TEXT_ROTATE_LIMIT = 3; 1.27 + 1.28 +/** 1.29 + * An instance of EditingSession tracks changes that have been made during the 1.30 + * modification of box model values. All of these changes can be reverted by 1.31 + * calling revert. 1.32 + * 1.33 + * @param doc A DOM document that can be used to test style rules. 1.34 + * @param rules An array of the style rules defined for the node being edited. 1.35 + * These should be in order of priority, least important first. 1.36 + */ 1.37 +function EditingSession(doc, rules) { 1.38 + this._doc = doc; 1.39 + this._rules = rules; 1.40 + this._modifications = new Map(); 1.41 +} 1.42 + 1.43 +EditingSession.prototype = { 1.44 + /** 1.45 + * Gets the value of a single property from the CSS rule. 1.46 + * 1.47 + * @param rule The CSS rule 1.48 + * @param property The name of the property 1.49 + */ 1.50 + getPropertyFromRule: function(rule, property) { 1.51 + let dummyStyle = this._element.style; 1.52 + 1.53 + dummyStyle.cssText = rule.cssText; 1.54 + return dummyStyle.getPropertyValue(property); 1.55 + }, 1.56 + 1.57 + /** 1.58 + * Returns the current value for a property as a string or the empty string if 1.59 + * no style rules affect the property. 1.60 + * 1.61 + * @param property The name of the property as a string 1.62 + */ 1.63 + getProperty: function(property) { 1.64 + // Create a hidden element for getPropertyFromRule to use 1.65 + let div = this._doc.createElement("div"); 1.66 + div.setAttribute("style", "display: none"); 1.67 + this._doc.body.appendChild(div); 1.68 + this._element = this._doc.createElement("p"); 1.69 + div.appendChild(this._element); 1.70 + 1.71 + // As the rules are in order of priority we can just iterate until we find 1.72 + // the first that defines a value for the property and return that. 1.73 + for (let rule of this._rules) { 1.74 + let value = this.getPropertyFromRule(rule, property); 1.75 + if (value !== "") { 1.76 + div.remove(); 1.77 + return value; 1.78 + } 1.79 + } 1.80 + div.remove(); 1.81 + return ""; 1.82 + }, 1.83 + 1.84 + /** 1.85 + * Sets a number of properties on the node. Returns a promise that will be 1.86 + * resolved when the modifications are complete. 1.87 + * 1.88 + * @param properties An array of properties, each is an object with name and 1.89 + * value properties. If the value is "" then the property 1.90 + * is removed. 1.91 + */ 1.92 + setProperties: function(properties) { 1.93 + let modifications = this._rules[0].startModifyingProperties(); 1.94 + 1.95 + for (let property of properties) { 1.96 + if (!this._modifications.has(property.name)) 1.97 + this._modifications.set(property.name, this.getPropertyFromRule(this._rules[0], property.name)); 1.98 + 1.99 + if (property.value == "") 1.100 + modifications.removeProperty(property.name); 1.101 + else 1.102 + modifications.setProperty(property.name, property.value, ""); 1.103 + } 1.104 + 1.105 + return modifications.apply().then(null, console.error); 1.106 + }, 1.107 + 1.108 + /** 1.109 + * Reverts all of the property changes made by this instance. Returns a 1.110 + * promise that will be resolved when complete. 1.111 + */ 1.112 + revert: function() { 1.113 + let modifications = this._rules[0].startModifyingProperties(); 1.114 + 1.115 + for (let [property, value] of this._modifications) { 1.116 + if (value != "") 1.117 + modifications.setProperty(property, value, ""); 1.118 + else 1.119 + modifications.removeProperty(property); 1.120 + } 1.121 + 1.122 + return modifications.apply().then(null, console.error); 1.123 + } 1.124 +}; 1.125 + 1.126 +function LayoutView(aInspector, aWindow) 1.127 +{ 1.128 + this.inspector = aInspector; 1.129 + 1.130 + // <browser> is not always available (for Chrome targets for example) 1.131 + if (this.inspector.target.tab) { 1.132 + this.browser = aInspector.target.tab.linkedBrowser; 1.133 + } 1.134 + 1.135 + this.doc = aWindow.document; 1.136 + this.sizeLabel = this.doc.querySelector(".size > span"); 1.137 + this.sizeHeadingLabel = this.doc.getElementById("element-size"); 1.138 + 1.139 + this.init(); 1.140 +} 1.141 + 1.142 +LayoutView.prototype = { 1.143 + init: function LV_init() { 1.144 + this.update = this.update.bind(this); 1.145 + this.onNewNode = this.onNewNode.bind(this); 1.146 + this.onNewSelection = this.onNewSelection.bind(this); 1.147 + this.inspector.selection.on("new-node-front", this.onNewSelection); 1.148 + this.inspector.sidebar.on("layoutview-selected", this.onNewNode); 1.149 + 1.150 + // Store for the different dimensions of the node. 1.151 + // 'selector' refers to the element that holds the value in view.xhtml; 1.152 + // 'property' is what we are measuring; 1.153 + // 'value' is the computed dimension, computed in update(). 1.154 + this.map = { 1.155 + position: {selector: "#element-position", 1.156 + property: "position", 1.157 + value: undefined}, 1.158 + marginTop: {selector: ".margin.top > span", 1.159 + property: "margin-top", 1.160 + value: undefined}, 1.161 + marginBottom: {selector: ".margin.bottom > span", 1.162 + property: "margin-bottom", 1.163 + value: undefined}, 1.164 + // margin-left is a shorthand for some internal properties, 1.165 + // margin-left-ltr-source and margin-left-rtl-source for example. The 1.166 + // real margin value we want is in margin-left-value 1.167 + marginLeft: {selector: ".margin.left > span", 1.168 + property: "margin-left", 1.169 + realProperty: "margin-left-value", 1.170 + value: undefined}, 1.171 + // margin-right behaves the same as margin-left 1.172 + marginRight: {selector: ".margin.right > span", 1.173 + property: "margin-right", 1.174 + realProperty: "margin-right-value", 1.175 + value: undefined}, 1.176 + paddingTop: {selector: ".padding.top > span", 1.177 + property: "padding-top", 1.178 + value: undefined}, 1.179 + paddingBottom: {selector: ".padding.bottom > span", 1.180 + property: "padding-bottom", 1.181 + value: undefined}, 1.182 + // padding-left behaves the same as margin-left 1.183 + paddingLeft: {selector: ".padding.left > span", 1.184 + property: "padding-left", 1.185 + realProperty: "padding-left-value", 1.186 + value: undefined}, 1.187 + // padding-right behaves the same as margin-left 1.188 + paddingRight: {selector: ".padding.right > span", 1.189 + property: "padding-right", 1.190 + realProperty: "padding-right-value", 1.191 + value: undefined}, 1.192 + borderTop: {selector: ".border.top > span", 1.193 + property: "border-top-width", 1.194 + value: undefined}, 1.195 + borderBottom: {selector: ".border.bottom > span", 1.196 + property: "border-bottom-width", 1.197 + value: undefined}, 1.198 + borderLeft: {selector: ".border.left > span", 1.199 + property: "border-left-width", 1.200 + value: undefined}, 1.201 + borderRight: {selector: ".border.right > span", 1.202 + property: "border-right-width", 1.203 + value: undefined}, 1.204 + }; 1.205 + 1.206 + // Make each element the dimensions editable 1.207 + for (let i in this.map) { 1.208 + if (i == "position") 1.209 + continue; 1.210 + 1.211 + let dimension = this.map[i]; 1.212 + editableItem({ element: this.doc.querySelector(dimension.selector) }, (element, event) => { 1.213 + this.initEditor(element, event, dimension); 1.214 + }); 1.215 + } 1.216 + 1.217 + this.onNewNode(); 1.218 + }, 1.219 + 1.220 + /** 1.221 + * Called when the user clicks on one of the editable values in the layoutview 1.222 + */ 1.223 + initEditor: function LV_initEditor(element, event, dimension) { 1.224 + let { property, realProperty } = dimension; 1.225 + if (!realProperty) 1.226 + realProperty = property; 1.227 + let session = new EditingSession(document, this.elementRules); 1.228 + let initialValue = session.getProperty(realProperty); 1.229 + 1.230 + let editor = new InplaceEditor({ 1.231 + element: element, 1.232 + initial: initialValue, 1.233 + 1.234 + start: (editor) => { 1.235 + editor.elt.parentNode.classList.add("editing"); 1.236 + }, 1.237 + 1.238 + change: (value) => { 1.239 + if (NUMERIC.test(value)) 1.240 + value += "px"; 1.241 + let properties = [ 1.242 + { name: property, value: value } 1.243 + ] 1.244 + 1.245 + if (property.substring(0, 7) == "border-") { 1.246 + let bprop = property.substring(0, property.length - 5) + "style"; 1.247 + let style = session.getProperty(bprop); 1.248 + if (!style || style == "none" || style == "hidden") 1.249 + properties.push({ name: bprop, value: "solid" }); 1.250 + } 1.251 + 1.252 + session.setProperties(properties); 1.253 + }, 1.254 + 1.255 + done: (value, commit) => { 1.256 + editor.elt.parentNode.classList.remove("editing"); 1.257 + if (!commit) 1.258 + session.revert(); 1.259 + } 1.260 + }, event); 1.261 + }, 1.262 + 1.263 + /** 1.264 + * Is the layoutview visible in the sidebar? 1.265 + */ 1.266 + isActive: function LV_isActive() { 1.267 + return this.inspector.sidebar.getCurrentTabID() == "layoutview"; 1.268 + }, 1.269 + 1.270 + /** 1.271 + * Destroy the nodes. Remove listeners. 1.272 + */ 1.273 + destroy: function LV_destroy() { 1.274 + this.inspector.sidebar.off("layoutview-selected", this.onNewNode); 1.275 + this.inspector.selection.off("new-node-front", this.onNewSelection); 1.276 + if (this.browser) { 1.277 + this.browser.removeEventListener("MozAfterPaint", this.update, true); 1.278 + } 1.279 + this.sizeHeadingLabel = null; 1.280 + this.sizeLabel = null; 1.281 + this.inspector = null; 1.282 + this.doc = null; 1.283 + }, 1.284 + 1.285 + /** 1.286 + * Selection 'new-node-front' event handler. 1.287 + */ 1.288 + onNewSelection: function() { 1.289 + let done = this.inspector.updating("layoutview"); 1.290 + this.onNewNode().then(done, (err) => { console.error(err); done() }); 1.291 + }, 1.292 + 1.293 + onNewNode: function LV_onNewNode() { 1.294 + if (this.isActive() && 1.295 + this.inspector.selection.isConnected() && 1.296 + this.inspector.selection.isElementNode()) { 1.297 + this.undim(); 1.298 + } else { 1.299 + this.dim(); 1.300 + } 1.301 + return this.update(); 1.302 + }, 1.303 + 1.304 + /** 1.305 + * Hide the layout boxes. No node are selected. 1.306 + */ 1.307 + dim: function LV_dim() { 1.308 + if (this.browser) { 1.309 + this.browser.removeEventListener("MozAfterPaint", this.update, true); 1.310 + } 1.311 + this.trackingPaint = false; 1.312 + this.doc.body.classList.add("dim"); 1.313 + this.dimmed = true; 1.314 + }, 1.315 + 1.316 + /** 1.317 + * Show the layout boxes. A node is selected. 1.318 + */ 1.319 + undim: function LV_undim() { 1.320 + if (!this.trackingPaint) { 1.321 + if (this.browser) { 1.322 + this.browser.addEventListener("MozAfterPaint", this.update, true); 1.323 + } 1.324 + this.trackingPaint = true; 1.325 + } 1.326 + this.doc.body.classList.remove("dim"); 1.327 + this.dimmed = false; 1.328 + }, 1.329 + 1.330 + /** 1.331 + * Compute the dimensions of the node and update the values in 1.332 + * the layoutview/view.xhtml document. Returns a promise that will be resolved 1.333 + * when complete. 1.334 + */ 1.335 + update: function LV_update() { 1.336 + let lastRequest = Task.spawn((function*() { 1.337 + if (!this.isActive() || 1.338 + !this.inspector.selection.isConnected() || 1.339 + !this.inspector.selection.isElementNode()) { 1.340 + return; 1.341 + } 1.342 + 1.343 + let node = this.inspector.selection.nodeFront; 1.344 + let layout = yield this.inspector.pageStyle.getLayout(node, { 1.345 + autoMargins: !this.dimmed 1.346 + }); 1.347 + let styleEntries = yield this.inspector.pageStyle.getApplied(node, {}); 1.348 + 1.349 + // If a subsequent request has been made, wait for that one instead. 1.350 + if (this._lastRequest != lastRequest) { 1.351 + return this._lastRequest; 1.352 + } 1.353 + 1.354 + this._lastRequest = null; 1.355 + let width = layout.width; 1.356 + let height = layout.height; 1.357 + let newLabel = width + "x" + height; 1.358 + if (this.sizeHeadingLabel.textContent != newLabel) { 1.359 + this.sizeHeadingLabel.textContent = newLabel; 1.360 + } 1.361 + 1.362 + // If the view is dimmed, no need to do anything more. 1.363 + if (this.dimmed) { 1.364 + this.inspector.emit("layoutview-updated"); 1.365 + return null; 1.366 + } 1.367 + 1.368 + for (let i in this.map) { 1.369 + let property = this.map[i].property; 1.370 + if (!(property in layout)) { 1.371 + // Depending on the actor version, some properties 1.372 + // might be missing. 1.373 + continue; 1.374 + } 1.375 + let parsedValue = parseInt(layout[property]); 1.376 + if (Number.isNaN(parsedValue)) { 1.377 + // Not a number. We use the raw string. 1.378 + // Useful for "position" for example. 1.379 + this.map[i].value = layout[property]; 1.380 + } else { 1.381 + this.map[i].value = parsedValue; 1.382 + } 1.383 + } 1.384 + 1.385 + let margins = layout.autoMargins; 1.386 + if ("top" in margins) this.map.marginTop.value = "auto"; 1.387 + if ("right" in margins) this.map.marginRight.value = "auto"; 1.388 + if ("bottom" in margins) this.map.marginBottom.value = "auto"; 1.389 + if ("left" in margins) this.map.marginLeft.value = "auto"; 1.390 + 1.391 + for (let i in this.map) { 1.392 + let selector = this.map[i].selector; 1.393 + let span = this.doc.querySelector(selector); 1.394 + if (span.textContent.length > 0 && 1.395 + span.textContent == this.map[i].value) { 1.396 + continue; 1.397 + } 1.398 + span.textContent = this.map[i].value; 1.399 + this.manageOverflowingText(span); 1.400 + } 1.401 + 1.402 + width -= this.map.borderLeft.value + this.map.borderRight.value + 1.403 + this.map.paddingLeft.value + this.map.paddingRight.value; 1.404 + 1.405 + height -= this.map.borderTop.value + this.map.borderBottom.value + 1.406 + this.map.paddingTop.value + this.map.paddingBottom.value; 1.407 + 1.408 + let newValue = width + "x" + height; 1.409 + if (this.sizeLabel.textContent != newValue) { 1.410 + this.sizeLabel.textContent = newValue; 1.411 + } 1.412 + 1.413 + this.elementRules = [e.rule for (e of styleEntries)]; 1.414 + 1.415 + this.inspector.emit("layoutview-updated"); 1.416 + }).bind(this)).then(null, console.error); 1.417 + 1.418 + return this._lastRequest = lastRequest; 1.419 + }, 1.420 + 1.421 + showBoxModel: function(options={}) { 1.422 + let toolbox = this.inspector.toolbox; 1.423 + let nodeFront = this.inspector.selection.nodeFront; 1.424 + 1.425 + toolbox.highlighterUtils.highlightNodeFront(nodeFront, options); 1.426 + }, 1.427 + 1.428 + hideBoxModel: function() { 1.429 + let toolbox = this.inspector.toolbox; 1.430 + 1.431 + toolbox.highlighterUtils.unhighlight(); 1.432 + }, 1.433 + 1.434 + manageOverflowingText: function(span) { 1.435 + let classList = span.parentNode.classList; 1.436 + 1.437 + if (classList.contains("left") || classList.contains("right")) { 1.438 + let force = span.textContent.length > LONG_TEXT_ROTATE_LIMIT; 1.439 + classList.toggle("rotate", force); 1.440 + } 1.441 + } 1.442 +}; 1.443 + 1.444 +let elts; 1.445 +let tooltip; 1.446 + 1.447 +let onmouseover = function(e) { 1.448 + let region = e.target.getAttribute("data-box"); 1.449 + 1.450 + tooltip.textContent = e.target.getAttribute("tooltip"); 1.451 + this.layoutview.showBoxModel({region: region}); 1.452 + 1.453 + return false; 1.454 +}.bind(window); 1.455 + 1.456 +let onmouseout = function(e) { 1.457 + tooltip.textContent = ""; 1.458 + this.layoutview.hideBoxModel(); 1.459 + 1.460 + return false; 1.461 +}.bind(window); 1.462 + 1.463 +window.setPanel = function(panel) { 1.464 + this.layoutview = new LayoutView(panel, window); 1.465 + 1.466 + // Tooltip mechanism 1.467 + elts = document.querySelectorAll("*[tooltip]"); 1.468 + tooltip = document.querySelector(".tooltip"); 1.469 + for (let i = 0; i < elts.length; i++) { 1.470 + let elt = elts[i]; 1.471 + elt.addEventListener("mouseover", onmouseover, true); 1.472 + elt.addEventListener("mouseout", onmouseout, true); 1.473 + } 1.474 + 1.475 + // Mark document as RTL or LTR: 1.476 + let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. 1.477 + getService(Ci.nsIXULChromeRegistry); 1.478 + let dir = chromeReg.isLocaleRTL("global"); 1.479 + document.body.setAttribute("dir", dir ? "rtl" : "ltr"); 1.480 + 1.481 + window.parent.postMessage("layoutview-ready", "*"); 1.482 +}; 1.483 + 1.484 +window.onunload = function() { 1.485 + this.layoutview.destroy(); 1.486 + if (elts) { 1.487 + for (let i = 0; i < elts.length; i++) { 1.488 + let elt = elts[i]; 1.489 + elt.removeEventListener("mouseover", onmouseover, true); 1.490 + elt.removeEventListener("mouseout", onmouseout, true); 1.491 + } 1.492 + } 1.493 +};