browser/devtools/styleinspector/computed-view.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/styleinspector/computed-view.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1388 @@
     1.4 +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set 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 +const {Cc, Ci, Cu} = require("chrome");
    1.11 +
    1.12 +const ToolDefinitions = require("main").Tools;
    1.13 +const {CssLogic} = require("devtools/styleinspector/css-logic");
    1.14 +const {ELEMENT_STYLE} = require("devtools/server/actors/styles");
    1.15 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    1.16 +const {EventEmitter} = require("devtools/toolkit/event-emitter");
    1.17 +const {OutputParser} = require("devtools/output-parser");
    1.18 +const {Tooltip} = require("devtools/shared/widgets/Tooltip");
    1.19 +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
    1.20 +const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
    1.21 +
    1.22 +Cu.import("resource://gre/modules/Services.jsm");
    1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.24 +Cu.import("resource://gre/modules/devtools/Templater.jsm");
    1.25 +
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    1.27 +                                  "resource://gre/modules/PluralForm.jsm");
    1.28 +
    1.29 +const FILTER_CHANGED_TIMEOUT = 300;
    1.30 +const HTML_NS = "http://www.w3.org/1999/xhtml";
    1.31 +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    1.32 +
    1.33 +/**
    1.34 + * Helper for long-running processes that should yield occasionally to
    1.35 + * the mainloop.
    1.36 + *
    1.37 + * @param {Window} aWin
    1.38 + *        Timeouts will be set on this window when appropriate.
    1.39 + * @param {Generator} aGenerator
    1.40 + *        Will iterate this generator.
    1.41 + * @param {object} aOptions
    1.42 + *        Options for the update process:
    1.43 + *          onItem {function} Will be called with the value of each iteration.
    1.44 + *          onBatch {function} Will be called after each batch of iterations,
    1.45 + *            before yielding to the main loop.
    1.46 + *          onDone {function} Will be called when iteration is complete.
    1.47 + *          onCancel {function} Will be called if the process is canceled.
    1.48 + *          threshold {int} How long to process before yielding, in ms.
    1.49 + *
    1.50 + * @constructor
    1.51 + */
    1.52 +function UpdateProcess(aWin, aGenerator, aOptions)
    1.53 +{
    1.54 +  this.win = aWin;
    1.55 +  this.iter = _Iterator(aGenerator);
    1.56 +  this.onItem = aOptions.onItem || function() {};
    1.57 +  this.onBatch = aOptions.onBatch || function () {};
    1.58 +  this.onDone = aOptions.onDone || function() {};
    1.59 +  this.onCancel = aOptions.onCancel || function() {};
    1.60 +  this.threshold = aOptions.threshold || 45;
    1.61 +
    1.62 +  this.canceled = false;
    1.63 +}
    1.64 +
    1.65 +UpdateProcess.prototype = {
    1.66 +  /**
    1.67 +   * Schedule a new batch on the main loop.
    1.68 +   */
    1.69 +  schedule: function UP_schedule()
    1.70 +  {
    1.71 +    if (this.canceled) {
    1.72 +      return;
    1.73 +    }
    1.74 +    this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0);
    1.75 +  },
    1.76 +
    1.77 +  /**
    1.78 +   * Cancel the running process.  onItem will not be called again,
    1.79 +   * and onCancel will be called.
    1.80 +   */
    1.81 +  cancel: function UP_cancel()
    1.82 +  {
    1.83 +    if (this._timeout) {
    1.84 +      this.win.clearTimeout(this._timeout);
    1.85 +      this._timeout = 0;
    1.86 +    }
    1.87 +    this.canceled = true;
    1.88 +    this.onCancel();
    1.89 +  },
    1.90 +
    1.91 +  _timeoutHandler: function UP_timeoutHandler() {
    1.92 +    this._timeout = null;
    1.93 +    try {
    1.94 +      this._runBatch();
    1.95 +      this.schedule();
    1.96 +    } catch(e) {
    1.97 +      if (e instanceof StopIteration) {
    1.98 +        this.onBatch();
    1.99 +        this.onDone();
   1.100 +        return;
   1.101 +      }
   1.102 +      console.error(e);
   1.103 +      throw e;
   1.104 +    }
   1.105 +  },
   1.106 +
   1.107 +  _runBatch: function Y_runBatch()
   1.108 +  {
   1.109 +    let time = Date.now();
   1.110 +    while(!this.canceled) {
   1.111 +      // Continue until iter.next() throws...
   1.112 +      let next = this.iter.next();
   1.113 +      this.onItem(next[1]);
   1.114 +      if ((Date.now() - time) > this.threshold) {
   1.115 +        this.onBatch();
   1.116 +        return;
   1.117 +      }
   1.118 +    }
   1.119 +  }
   1.120 +};
   1.121 +
   1.122 +/**
   1.123 + * CssHtmlTree is a panel that manages the display of a table sorted by style.
   1.124 + * There should be one instance of CssHtmlTree per style display (of which there
   1.125 + * will generally only be one).
   1.126 + *
   1.127 + * @params {StyleInspector} aStyleInspector The owner of this CssHtmlTree
   1.128 + * @param {PageStyleFront} aPageStyle
   1.129 + *        Front for the page style actor that will be providing
   1.130 + *        the style information.
   1.131 + *
   1.132 + * @constructor
   1.133 + */
   1.134 +function CssHtmlTree(aStyleInspector, aPageStyle)
   1.135 +{
   1.136 +  this.styleWindow = aStyleInspector.window;
   1.137 +  this.styleDocument = aStyleInspector.window.document;
   1.138 +  this.styleInspector = aStyleInspector;
   1.139 +  this.pageStyle = aPageStyle;
   1.140 +  this.propertyViews = [];
   1.141 +
   1.142 +  this._outputParser = new OutputParser();
   1.143 +
   1.144 +  let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
   1.145 +    getService(Ci.nsIXULChromeRegistry);
   1.146 +  this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
   1.147 +
   1.148 +  // Create bound methods.
   1.149 +  this.focusWindow = this.focusWindow.bind(this);
   1.150 +  this._onContextMenu = this._onContextMenu.bind(this);
   1.151 +  this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   1.152 +  this._onSelectAll = this._onSelectAll.bind(this);
   1.153 +  this._onClick = this._onClick.bind(this);
   1.154 +  this._onCopy = this._onCopy.bind(this);
   1.155 +
   1.156 +  this.styleDocument.addEventListener("copy", this._onCopy);
   1.157 +  this.styleDocument.addEventListener("mousedown", this.focusWindow);
   1.158 +  this.styleDocument.addEventListener("contextmenu", this._onContextMenu);
   1.159 +
   1.160 +  // Nodes used in templating
   1.161 +  this.root = this.styleDocument.getElementById("root");
   1.162 +  this.templateRoot = this.styleDocument.getElementById("templateRoot");
   1.163 +  this.propertyContainer = this.styleDocument.getElementById("propertyContainer");
   1.164 +
   1.165 +  // Listen for click events
   1.166 +  this.propertyContainer.addEventListener("click", this._onClick, false);
   1.167 +
   1.168 +  // No results text.
   1.169 +  this.noResults = this.styleDocument.getElementById("noResults");
   1.170 +
   1.171 +  // Refresh panel when color unit changed.
   1.172 +  this._handlePrefChange = this._handlePrefChange.bind(this);
   1.173 +  gDevTools.on("pref-changed", this._handlePrefChange);
   1.174 +
   1.175 +  // Refresh panel when pref for showing original sources changes
   1.176 +  this._updateSourceLinks = this._updateSourceLinks.bind(this);
   1.177 +  this._prefObserver = new PrefObserver("devtools.");
   1.178 +  this._prefObserver.on(PREF_ORIG_SOURCES, this._updateSourceLinks);
   1.179 +
   1.180 +  CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
   1.181 +
   1.182 +  // The element that we're inspecting, and the document that it comes from.
   1.183 +  this.viewedElement = null;
   1.184 +
   1.185 +  // Properties preview tooltip
   1.186 +  this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc);
   1.187 +  this.tooltip.startTogglingOnHover(this.propertyContainer,
   1.188 +    this._onTooltipTargetHover.bind(this));
   1.189 +
   1.190 +  this._buildContextMenu();
   1.191 +  this.createStyleViews();
   1.192 +}
   1.193 +
   1.194 +/**
   1.195 + * Memoized lookup of a l10n string from a string bundle.
   1.196 + * @param {string} aName The key to lookup.
   1.197 + * @returns A localized version of the given key.
   1.198 + */
   1.199 +CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
   1.200 +{
   1.201 +  try {
   1.202 +    return CssHtmlTree._strings.GetStringFromName(aName);
   1.203 +  } catch (ex) {
   1.204 +    Services.console.logStringMessage("Error reading '" + aName + "'");
   1.205 +    throw new Error("l10n error with " + aName);
   1.206 +  }
   1.207 +};
   1.208 +
   1.209 +/**
   1.210 + * Clone the given template node, and process it by resolving ${} references
   1.211 + * in the template.
   1.212 + *
   1.213 + * @param {nsIDOMElement} aTemplate the template note to use.
   1.214 + * @param {nsIDOMElement} aDestination the destination node where the
   1.215 + * processed nodes will be displayed.
   1.216 + * @param {object} aData the data to pass to the template.
   1.217 + * @param {Boolean} aPreserveDestination If true then the template will be
   1.218 + * appended to aDestination's content else aDestination.innerHTML will be
   1.219 + * cleared before the template is appended.
   1.220 + */
   1.221 +CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
   1.222 +                                  aDestination, aData, aPreserveDestination)
   1.223 +{
   1.224 +  if (!aPreserveDestination) {
   1.225 +    aDestination.innerHTML = "";
   1.226 +  }
   1.227 +
   1.228 +  // All the templater does is to populate a given DOM tree with the given
   1.229 +  // values, so we need to clone the template first.
   1.230 +  let duplicated = aTemplate.cloneNode(true);
   1.231 +
   1.232 +  // See https://github.com/mozilla/domtemplate/blob/master/README.md
   1.233 +  // for docs on the template() function
   1.234 +  template(duplicated, aData, { allowEval: true });
   1.235 +  while (duplicated.firstChild) {
   1.236 +    aDestination.appendChild(duplicated.firstChild);
   1.237 +  }
   1.238 +};
   1.239 +
   1.240 +XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
   1.241 +        .createBundle("chrome://global/locale/devtools/styleinspector.properties"));
   1.242 +
   1.243 +XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
   1.244 +  return Cc["@mozilla.org/widget/clipboardhelper;1"].
   1.245 +    getService(Ci.nsIClipboardHelper);
   1.246 +});
   1.247 +
   1.248 +CssHtmlTree.prototype = {
   1.249 +  // Cache the list of properties that match the selected element.
   1.250 +  _matchedProperties: null,
   1.251 +
   1.252 +  // Used for cancelling timeouts in the style filter.
   1.253 +  _filterChangedTimeout: null,
   1.254 +
   1.255 +  // The search filter
   1.256 +  searchField: null,
   1.257 +
   1.258 +  // Reference to the "Include browser styles" checkbox.
   1.259 +  includeBrowserStylesCheckbox: null,
   1.260 +
   1.261 +  // Holds the ID of the panelRefresh timeout.
   1.262 +  _panelRefreshTimeout: null,
   1.263 +
   1.264 +  // Toggle for zebra striping
   1.265 +  _darkStripe: true,
   1.266 +
   1.267 +  // Number of visible properties
   1.268 +  numVisibleProperties: 0,
   1.269 +
   1.270 +  setPageStyle: function(pageStyle) {
   1.271 +    this.pageStyle = pageStyle;
   1.272 +  },
   1.273 +
   1.274 +  get includeBrowserStyles()
   1.275 +  {
   1.276 +    return this.includeBrowserStylesCheckbox.checked;
   1.277 +  },
   1.278 +
   1.279 +  _handlePrefChange: function(event, data) {
   1.280 +    if (this._computed && (data.pref == "devtools.defaultColorUnit" ||
   1.281 +        data.pref == PREF_ORIG_SOURCES)) {
   1.282 +      this.refreshPanel();
   1.283 +    }
   1.284 +  },
   1.285 +
   1.286 +  /**
   1.287 +   * Update the highlighted element. The CssHtmlTree panel will show the style
   1.288 +   * information for the given element.
   1.289 +   * @param {nsIDOMElement} aElement The highlighted node to get styles for.
   1.290 +   *
   1.291 +   * @returns a promise that will be resolved when highlighting is complete.
   1.292 +   */
   1.293 +  highlight: function(aElement) {
   1.294 +    if (!aElement) {
   1.295 +      this.viewedElement = null;
   1.296 +      this.noResults.hidden = false;
   1.297 +
   1.298 +      if (this._refreshProcess) {
   1.299 +        this._refreshProcess.cancel();
   1.300 +      }
   1.301 +      // Hiding all properties
   1.302 +      for (let propView of this.propertyViews) {
   1.303 +        propView.refresh();
   1.304 +      }
   1.305 +      return promise.resolve(undefined);
   1.306 +    }
   1.307 +
   1.308 +    this.tooltip.hide();
   1.309 +
   1.310 +    if (aElement === this.viewedElement) {
   1.311 +      return promise.resolve(undefined);
   1.312 +    }
   1.313 +
   1.314 +    this.viewedElement = aElement;
   1.315 +    this.refreshSourceFilter();
   1.316 +
   1.317 +    return this.refreshPanel();
   1.318 +  },
   1.319 +
   1.320 +  _createPropertyViews: function()
   1.321 +  {
   1.322 +    if (this._createViewsPromise) {
   1.323 +      return this._createViewsPromise;
   1.324 +    }
   1.325 +
   1.326 +    let deferred = promise.defer();
   1.327 +    this._createViewsPromise = deferred.promise;
   1.328 +
   1.329 +    this.refreshSourceFilter();
   1.330 +    this.numVisibleProperties = 0;
   1.331 +    let fragment = this.styleDocument.createDocumentFragment();
   1.332 +
   1.333 +    this._createViewsProcess = new UpdateProcess(this.styleWindow, CssHtmlTree.propertyNames, {
   1.334 +      onItem: (aPropertyName) => {
   1.335 +        // Per-item callback.
   1.336 +        let propView = new PropertyView(this, aPropertyName);
   1.337 +        fragment.appendChild(propView.buildMain());
   1.338 +        fragment.appendChild(propView.buildSelectorContainer());
   1.339 +
   1.340 +        if (propView.visible) {
   1.341 +          this.numVisibleProperties++;
   1.342 +        }
   1.343 +        this.propertyViews.push(propView);
   1.344 +      },
   1.345 +      onCancel: () => {
   1.346 +        deferred.reject("_createPropertyViews cancelled");
   1.347 +      },
   1.348 +      onDone: () => {
   1.349 +        // Completed callback.
   1.350 +        this.propertyContainer.appendChild(fragment);
   1.351 +        this.noResults.hidden = this.numVisibleProperties > 0;
   1.352 +        deferred.resolve(undefined);
   1.353 +      }
   1.354 +    });
   1.355 +
   1.356 +    this._createViewsProcess.schedule();
   1.357 +    return deferred.promise;
   1.358 +  },
   1.359 +
   1.360 +  /**
   1.361 +   * Refresh the panel content.
   1.362 +   */
   1.363 +  refreshPanel: function CssHtmlTree_refreshPanel()
   1.364 +  {
   1.365 +    if (!this.viewedElement) {
   1.366 +      return promise.resolve();
   1.367 +    }
   1.368 +
   1.369 +    return promise.all([
   1.370 +      this._createPropertyViews(),
   1.371 +      this.pageStyle.getComputed(this.viewedElement, {
   1.372 +        filter: this._sourceFilter,
   1.373 +        onlyMatched: !this.includeBrowserStyles,
   1.374 +        markMatched: true
   1.375 +      })
   1.376 +    ]).then(([createViews, computed]) => {
   1.377 +      this._matchedProperties = new Set;
   1.378 +      for (let name in computed) {
   1.379 +        if (computed[name].matched) {
   1.380 +          this._matchedProperties.add(name);
   1.381 +        }
   1.382 +      }
   1.383 +      this._computed = computed;
   1.384 +
   1.385 +      if (this._refreshProcess) {
   1.386 +        this._refreshProcess.cancel();
   1.387 +      }
   1.388 +
   1.389 +      this.noResults.hidden = true;
   1.390 +
   1.391 +      // Reset visible property count
   1.392 +      this.numVisibleProperties = 0;
   1.393 +
   1.394 +      // Reset zebra striping.
   1.395 +      this._darkStripe = true;
   1.396 +
   1.397 +      let deferred = promise.defer();
   1.398 +      this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
   1.399 +        onItem: (aPropView) => {
   1.400 +          aPropView.refresh();
   1.401 +        },
   1.402 +        onDone: () => {
   1.403 +          this._refreshProcess = null;
   1.404 +          this.noResults.hidden = this.numVisibleProperties > 0;
   1.405 +          this.styleInspector.inspector.emit("computed-view-refreshed");
   1.406 +          deferred.resolve(undefined);
   1.407 +        }
   1.408 +      });
   1.409 +      this._refreshProcess.schedule();
   1.410 +      return deferred.promise;
   1.411 +    }).then(null, (err) => console.error(err));
   1.412 +  },
   1.413 +
   1.414 +  /**
   1.415 +   * Called when the user enters a search term.
   1.416 +   *
   1.417 +   * @param {Event} aEvent the DOM Event object.
   1.418 +   */
   1.419 +  filterChanged: function CssHtmlTree_filterChanged(aEvent)
   1.420 +  {
   1.421 +    let win = this.styleWindow;
   1.422 +
   1.423 +    if (this._filterChangedTimeout) {
   1.424 +      win.clearTimeout(this._filterChangedTimeout);
   1.425 +    }
   1.426 +
   1.427 +    this._filterChangedTimeout = win.setTimeout(function() {
   1.428 +      this.refreshPanel();
   1.429 +      this._filterChangeTimeout = null;
   1.430 +    }.bind(this), FILTER_CHANGED_TIMEOUT);
   1.431 +  },
   1.432 +
   1.433 +  /**
   1.434 +   * The change event handler for the includeBrowserStyles checkbox.
   1.435 +   *
   1.436 +   * @param {Event} aEvent the DOM Event object.
   1.437 +   */
   1.438 +  includeBrowserStylesChanged:
   1.439 +  function CssHtmltree_includeBrowserStylesChanged(aEvent)
   1.440 +  {
   1.441 +    this.refreshSourceFilter();
   1.442 +    this.refreshPanel();
   1.443 +  },
   1.444 +
   1.445 +  /**
   1.446 +   * When includeBrowserStyles.checked is false we only display properties that
   1.447 +   * have matched selectors and have been included by the document or one of the
   1.448 +   * document's stylesheets. If .checked is false we display all properties
   1.449 +   * including those that come from UA stylesheets.
   1.450 +   */
   1.451 +  refreshSourceFilter: function CssHtmlTree_setSourceFilter()
   1.452 +  {
   1.453 +    this._matchedProperties = null;
   1.454 +    this._sourceFilter = this.includeBrowserStyles ?
   1.455 +                                 CssLogic.FILTER.UA :
   1.456 +                                 CssLogic.FILTER.USER;
   1.457 +  },
   1.458 +
   1.459 +  _updateSourceLinks: function CssHtmlTree__updateSourceLinks()
   1.460 +  {
   1.461 +    for (let propView of this.propertyViews) {
   1.462 +      propView.updateSourceLinks();
   1.463 +    }
   1.464 +  },
   1.465 +
   1.466 +  /**
   1.467 +   * The CSS as displayed by the UI.
   1.468 +   */
   1.469 +  createStyleViews: function CssHtmlTree_createStyleViews()
   1.470 +  {
   1.471 +    if (CssHtmlTree.propertyNames) {
   1.472 +      return;
   1.473 +    }
   1.474 +
   1.475 +    CssHtmlTree.propertyNames = [];
   1.476 +
   1.477 +    // Here we build and cache a list of css properties supported by the browser
   1.478 +    // We could use any element but let's use the main document's root element
   1.479 +    let styles = this.styleWindow.getComputedStyle(this.styleDocument.documentElement);
   1.480 +    let mozProps = [];
   1.481 +    for (let i = 0, numStyles = styles.length; i < numStyles; i++) {
   1.482 +      let prop = styles.item(i);
   1.483 +      if (prop.charAt(0) == "-") {
   1.484 +        mozProps.push(prop);
   1.485 +      } else {
   1.486 +        CssHtmlTree.propertyNames.push(prop);
   1.487 +      }
   1.488 +    }
   1.489 +
   1.490 +    CssHtmlTree.propertyNames.sort();
   1.491 +    CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames,
   1.492 +      mozProps.sort());
   1.493 +
   1.494 +    this._createPropertyViews();
   1.495 +  },
   1.496 +
   1.497 +  /**
   1.498 +   * Get a set of properties that have matched selectors.
   1.499 +   *
   1.500 +   * @return {Set} If a property name is in the set, it has matching selectors.
   1.501 +   */
   1.502 +  get matchedProperties()
   1.503 +  {
   1.504 +    return this._matchedProperties || new Set;
   1.505 +  },
   1.506 +
   1.507 +  /**
   1.508 +   * Focus the window on mousedown.
   1.509 +   *
   1.510 +   * @param aEvent The event object
   1.511 +   */
   1.512 +  focusWindow: function(aEvent)
   1.513 +  {
   1.514 +    let win = this.styleDocument.defaultView;
   1.515 +    win.focus();
   1.516 +  },
   1.517 +
   1.518 +  /**
   1.519 +   * Executed by the tooltip when the pointer hovers over an element of the view.
   1.520 +   * Used to decide whether the tooltip should be shown or not and to actually
   1.521 +   * put content in it.
   1.522 +   * Checks if the hovered target is a css value we support tooltips for.
   1.523 +   */
   1.524 +  _onTooltipTargetHover: function(target)
   1.525 +  {
   1.526 +    let inspector = this.styleInspector.inspector;
   1.527 +
   1.528 +    // Test for image url
   1.529 +    if (target.classList.contains("theme-link") && inspector.hasUrlToImageDataResolver) {
   1.530 +      let propValue = target.parentNode;
   1.531 +      let propName = propValue.parentNode.querySelector(".property-name");
   1.532 +      if (propName.textContent === "background-image") {
   1.533 +        let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
   1.534 +        let uri = CssLogic.getBackgroundImageUriFromProperty(propValue.textContent);
   1.535 +        return this.tooltip.setRelativeImageContent(uri, inspector.inspector, maxDim);
   1.536 +      }
   1.537 +    }
   1.538 +
   1.539 +    if (target.classList.contains("property-value")) {
   1.540 +      let propValue = target;
   1.541 +      let propName = target.parentNode.querySelector(".property-name");
   1.542 +
   1.543 +      // Test for css transform
   1.544 +      if (propName.textContent === "transform") {
   1.545 +        return this.tooltip.setCssTransformContent(propValue.textContent,
   1.546 +          this.pageStyle, this.viewedElement);
   1.547 +      }
   1.548 +
   1.549 +      // Test for font family
   1.550 +      if (propName.textContent === "font-family") {
   1.551 +        this.tooltip.setFontFamilyContent(propValue.textContent);
   1.552 +        return true;
   1.553 +      }
   1.554 +    }
   1.555 +
   1.556 +    // If the target isn't one that should receive a tooltip, signal it by rejecting
   1.557 +    // a promise
   1.558 +    return promise.reject();
   1.559 +  },
   1.560 +
   1.561 +  /**
   1.562 +   * Create a context menu.
   1.563 +   */
   1.564 +  _buildContextMenu: function()
   1.565 +  {
   1.566 +    let doc = this.styleDocument.defaultView.parent.document;
   1.567 +
   1.568 +    this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup");
   1.569 +    this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
   1.570 +    this._contextmenu.id = "computed-view-context-menu";
   1.571 +
   1.572 +    // Select All
   1.573 +    this.menuitemSelectAll = createMenuItem(this._contextmenu, {
   1.574 +      label: "computedView.contextmenu.selectAll",
   1.575 +      accesskey: "computedView.contextmenu.selectAll.accessKey",
   1.576 +      command: this._onSelectAll
   1.577 +    });
   1.578 +
   1.579 +    // Copy
   1.580 +    this.menuitemCopy = createMenuItem(this._contextmenu, {
   1.581 +      label: "computedView.contextmenu.copy",
   1.582 +      accesskey: "computedView.contextmenu.copy.accessKey",
   1.583 +      command: this._onCopy
   1.584 +    });
   1.585 +
   1.586 +    // Show Original Sources
   1.587 +    this.menuitemSources= createMenuItem(this._contextmenu, {
   1.588 +      label: "ruleView.contextmenu.showOrigSources",
   1.589 +      accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
   1.590 +      command: this._onToggleOrigSources
   1.591 +    });
   1.592 +
   1.593 +    let popupset = doc.documentElement.querySelector("popupset");
   1.594 +    if (!popupset) {
   1.595 +      popupset = doc.createElementNS(XUL_NS, "popupset");
   1.596 +      doc.documentElement.appendChild(popupset);
   1.597 +    }
   1.598 +    popupset.appendChild(this._contextmenu);
   1.599 +  },
   1.600 +
   1.601 +  /**
   1.602 +   * Update the context menu. This means enabling or disabling menuitems as
   1.603 +   * appropriate.
   1.604 +   */
   1.605 +  _contextMenuUpdate: function()
   1.606 +  {
   1.607 +    let win = this.styleDocument.defaultView;
   1.608 +    let disable = win.getSelection().isCollapsed;
   1.609 +    this.menuitemCopy.disabled = disable;
   1.610 +
   1.611 +    let label = "ruleView.contextmenu.showOrigSources";
   1.612 +    if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
   1.613 +      label = "ruleView.contextmenu.showCSSSources";
   1.614 +    }
   1.615 +    this.menuitemSources.setAttribute("label",
   1.616 +                                      CssHtmlTree.l10n(label));
   1.617 +
   1.618 +    let accessKey = label + ".accessKey";
   1.619 +    this.menuitemSources.setAttribute("accesskey",
   1.620 +                                      CssHtmlTree.l10n(accessKey));
   1.621 +  },
   1.622 +
   1.623 +  /**
   1.624 +   * Context menu handler.
   1.625 +   */
   1.626 +  _onContextMenu: function(event) {
   1.627 +    try {
   1.628 +      this.styleDocument.defaultView.focus();
   1.629 +      this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
   1.630 +    } catch(e) {
   1.631 +      console.error(e);
   1.632 +    }
   1.633 +  },
   1.634 +
   1.635 +  /**
   1.636 +   * Select all text.
   1.637 +   */
   1.638 +  _onSelectAll: function()
   1.639 +  {
   1.640 +    try {
   1.641 +      let win = this.styleDocument.defaultView;
   1.642 +      let selection = win.getSelection();
   1.643 +
   1.644 +      selection.selectAllChildren(this.styleDocument.documentElement);
   1.645 +    } catch(e) {
   1.646 +      console.error(e);
   1.647 +    }
   1.648 +  },
   1.649 +
   1.650 +  _onClick: function(event) {
   1.651 +    let target = event.target;
   1.652 +
   1.653 +    if (target.nodeName === "a") {
   1.654 +      event.stopPropagation();
   1.655 +      event.preventDefault();
   1.656 +      let browserWin = this.styleInspector.inspector.target
   1.657 +                           .tab.ownerDocument.defaultView;
   1.658 +      browserWin.openUILinkIn(target.href, "tab");
   1.659 +    }
   1.660 +  },
   1.661 +
   1.662 +  /**
   1.663 +   * Copy selected text.
   1.664 +   *
   1.665 +   * @param event The event object
   1.666 +   */
   1.667 +  _onCopy: function(event)
   1.668 +  {
   1.669 +    try {
   1.670 +      let win = this.styleDocument.defaultView;
   1.671 +      let text = win.getSelection().toString().trim();
   1.672 +
   1.673 +      // Tidy up block headings by moving CSS property names and their values onto
   1.674 +      // the same line and inserting a colon between them.
   1.675 +      let textArray = text.split(/[\r\n]+/);
   1.676 +      let result = "";
   1.677 +
   1.678 +      // Parse text array to output string.
   1.679 +      if (textArray.length > 1) {
   1.680 +        for (let prop of textArray) {
   1.681 +          if (CssHtmlTree.propertyNames.indexOf(prop) !== -1) {
   1.682 +            // Property name
   1.683 +            result += prop;
   1.684 +          } else {
   1.685 +            // Property value
   1.686 +            result += ": " + prop;
   1.687 +            if (result.length > 0) {
   1.688 +              result += ";\n";
   1.689 +            }
   1.690 +          }
   1.691 +        }
   1.692 +      } else {
   1.693 +        // Short text fragment.
   1.694 +        result = textArray[0];
   1.695 +      }
   1.696 +
   1.697 +      clipboardHelper.copyString(result, this.styleDocument);
   1.698 +
   1.699 +      if (event) {
   1.700 +        event.preventDefault();
   1.701 +      }
   1.702 +    } catch(e) {
   1.703 +      console.error(e);
   1.704 +    }
   1.705 +  },
   1.706 +
   1.707 +  /**
   1.708 +   *  Toggle the original sources pref.
   1.709 +   */
   1.710 +  _onToggleOrigSources: function()
   1.711 +  {
   1.712 +    let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
   1.713 +    Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   1.714 +  },
   1.715 +
   1.716 +  /**
   1.717 +   * Destructor for CssHtmlTree.
   1.718 +   */
   1.719 +  destroy: function CssHtmlTree_destroy()
   1.720 +  {
   1.721 +    delete this.viewedElement;
   1.722 +    delete this._outputParser;
   1.723 +
   1.724 +    // Remove event listeners
   1.725 +    this.includeBrowserStylesCheckbox.removeEventListener("command",
   1.726 +      this.includeBrowserStylesChanged);
   1.727 +    this.searchField.removeEventListener("command", this.filterChanged);
   1.728 +    gDevTools.off("pref-changed", this._handlePrefChange);
   1.729 +
   1.730 +    this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks);
   1.731 +    this._prefObserver.destroy();
   1.732 +
   1.733 +    // Cancel tree construction
   1.734 +    if (this._createViewsProcess) {
   1.735 +      this._createViewsProcess.cancel();
   1.736 +    }
   1.737 +    if (this._refreshProcess) {
   1.738 +      this._refreshProcess.cancel();
   1.739 +    }
   1.740 +
   1.741 +    this.propertyContainer.removeEventListener("click", this._onClick, false);
   1.742 +
   1.743 +    // Remove context menu
   1.744 +    if (this._contextmenu) {
   1.745 +      // Destroy the Select All menuitem.
   1.746 +      this.menuitemCopy.removeEventListener("command", this._onCopy);
   1.747 +      this.menuitemCopy = null;
   1.748 +
   1.749 +      // Destroy the Copy menuitem.
   1.750 +      this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
   1.751 +      this.menuitemSelectAll = null;
   1.752 +
   1.753 +      // Destroy the context menu.
   1.754 +      this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
   1.755 +      this._contextmenu.parentNode.removeChild(this._contextmenu);
   1.756 +      this._contextmenu = null;
   1.757 +    }
   1.758 +
   1.759 +    this.tooltip.stopTogglingOnHover(this.propertyContainer);
   1.760 +    this.tooltip.destroy();
   1.761 +
   1.762 +    // Remove bound listeners
   1.763 +    this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
   1.764 +    this.styleDocument.removeEventListener("copy", this._onCopy);
   1.765 +    this.styleDocument.removeEventListener("mousedown", this.focusWindow);
   1.766 +
   1.767 +    // Nodes used in templating
   1.768 +    delete this.root;
   1.769 +    delete this.propertyContainer;
   1.770 +    delete this.panel;
   1.771 +
   1.772 +    // The document in which we display the results (csshtmltree.xul).
   1.773 +    delete this.styleDocument;
   1.774 +
   1.775 +    for (let propView of this.propertyViews)  {
   1.776 +      propView.destroy();
   1.777 +    }
   1.778 +
   1.779 +    // The element that we're inspecting, and the document that it comes from.
   1.780 +    delete this.propertyViews;
   1.781 +    delete this.styleWindow;
   1.782 +    delete this.styleDocument;
   1.783 +    delete this.styleInspector;
   1.784 +  }
   1.785 +};
   1.786 +
   1.787 +function PropertyInfo(aTree, aName) {
   1.788 +  this.tree = aTree;
   1.789 +  this.name = aName;
   1.790 +}
   1.791 +PropertyInfo.prototype = {
   1.792 +  get value() {
   1.793 +    if (this.tree._computed) {
   1.794 +      let value = this.tree._computed[this.name].value;
   1.795 +      return value;
   1.796 +    }
   1.797 +  }
   1.798 +};
   1.799 +
   1.800 +function createMenuItem(aMenu, aAttributes)
   1.801 +{
   1.802 +  let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
   1.803 +
   1.804 +  item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label));
   1.805 +  item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey));
   1.806 +  item.addEventListener("command", aAttributes.command);
   1.807 +
   1.808 +  aMenu.appendChild(item);
   1.809 +
   1.810 +  return item;
   1.811 +}
   1.812 +
   1.813 +/**
   1.814 + * A container to give easy access to property data from the template engine.
   1.815 + *
   1.816 + * @constructor
   1.817 + * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with.
   1.818 + * @param {string} aName the CSS property name for which this PropertyView
   1.819 + * instance will render the rules.
   1.820 + */
   1.821 +function PropertyView(aTree, aName)
   1.822 +{
   1.823 +  this.tree = aTree;
   1.824 +  this.name = aName;
   1.825 +  this.getRTLAttr = aTree.getRTLAttr;
   1.826 +
   1.827 +  this.link = "https://developer.mozilla.org/CSS/" + aName;
   1.828 +
   1.829 +  this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors");
   1.830 +  this._propertyInfo = new PropertyInfo(aTree, aName);
   1.831 +}
   1.832 +
   1.833 +PropertyView.prototype = {
   1.834 +  // The parent element which contains the open attribute
   1.835 +  element: null,
   1.836 +
   1.837 +  // Property header node
   1.838 +  propertyHeader: null,
   1.839 +
   1.840 +  // Destination for property names
   1.841 +  nameNode: null,
   1.842 +
   1.843 +  // Destination for property values
   1.844 +  valueNode: null,
   1.845 +
   1.846 +  // Are matched rules expanded?
   1.847 +  matchedExpanded: false,
   1.848 +
   1.849 +  // Matched selector container
   1.850 +  matchedSelectorsContainer: null,
   1.851 +
   1.852 +  // Matched selector expando
   1.853 +  matchedExpander: null,
   1.854 +
   1.855 +  // Cache for matched selector views
   1.856 +  _matchedSelectorViews: null,
   1.857 +
   1.858 +  // The previously selected element used for the selector view caches
   1.859 +  prevViewedElement: null,
   1.860 +
   1.861 +  /**
   1.862 +   * Get the computed style for the current property.
   1.863 +   *
   1.864 +   * @return {string} the computed style for the current property of the
   1.865 +   * currently highlighted element.
   1.866 +   */
   1.867 +  get value()
   1.868 +  {
   1.869 +    return this.propertyInfo.value;
   1.870 +  },
   1.871 +
   1.872 +  /**
   1.873 +   * An easy way to access the CssPropertyInfo behind this PropertyView.
   1.874 +   */
   1.875 +  get propertyInfo()
   1.876 +  {
   1.877 +    return this._propertyInfo;
   1.878 +  },
   1.879 +
   1.880 +  /**
   1.881 +   * Does the property have any matched selectors?
   1.882 +   */
   1.883 +  get hasMatchedSelectors()
   1.884 +  {
   1.885 +    return this.tree.matchedProperties.has(this.name);
   1.886 +  },
   1.887 +
   1.888 +  /**
   1.889 +   * Should this property be visible?
   1.890 +   */
   1.891 +  get visible()
   1.892 +  {
   1.893 +    if (!this.tree.viewedElement) {
   1.894 +      return false;
   1.895 +    }
   1.896 +
   1.897 +    if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
   1.898 +      return false;
   1.899 +    }
   1.900 +
   1.901 +    let searchTerm = this.tree.searchField.value.toLowerCase();
   1.902 +    if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 &&
   1.903 +      this.value.toLowerCase().indexOf(searchTerm) == -1) {
   1.904 +      return false;
   1.905 +    }
   1.906 +
   1.907 +    return true;
   1.908 +  },
   1.909 +
   1.910 +  /**
   1.911 +   * Returns the className that should be assigned to the propertyView.
   1.912 +   * @return string
   1.913 +   */
   1.914 +  get propertyHeaderClassName()
   1.915 +  {
   1.916 +    if (this.visible) {
   1.917 +      let isDark = this.tree._darkStripe = !this.tree._darkStripe;
   1.918 +      return isDark ? "property-view theme-bg-darker" : "property-view";
   1.919 +    }
   1.920 +    return "property-view-hidden";
   1.921 +  },
   1.922 +
   1.923 +  /**
   1.924 +   * Returns the className that should be assigned to the propertyView content
   1.925 +   * container.
   1.926 +   * @return string
   1.927 +   */
   1.928 +  get propertyContentClassName()
   1.929 +  {
   1.930 +    if (this.visible) {
   1.931 +      let isDark = this.tree._darkStripe;
   1.932 +      return isDark ? "property-content theme-bg-darker" : "property-content";
   1.933 +    }
   1.934 +    return "property-content-hidden";
   1.935 +  },
   1.936 +
   1.937 +  /**
   1.938 +   * Build the markup for on computed style
   1.939 +   * @return Element
   1.940 +   */
   1.941 +  buildMain: function PropertyView_buildMain()
   1.942 +  {
   1.943 +    let doc = this.tree.styleDocument;
   1.944 +
   1.945 +    // Build the container element
   1.946 +    this.onMatchedToggle = this.onMatchedToggle.bind(this);
   1.947 +    this.element = doc.createElementNS(HTML_NS, "div");
   1.948 +    this.element.setAttribute("class", this.propertyHeaderClassName);
   1.949 +    this.element.addEventListener("dblclick", this.onMatchedToggle, false);
   1.950 +
   1.951 +    // Make it keyboard navigable
   1.952 +    this.element.setAttribute("tabindex", "0");
   1.953 +    this.onKeyDown = (aEvent) => {
   1.954 +      let keyEvent = Ci.nsIDOMKeyEvent;
   1.955 +      if (aEvent.keyCode == keyEvent.DOM_VK_F1) {
   1.956 +        this.mdnLinkClick();
   1.957 +      }
   1.958 +      if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
   1.959 +        aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
   1.960 +        this.onMatchedToggle(aEvent);
   1.961 +      }
   1.962 +    };
   1.963 +    this.element.addEventListener("keydown", this.onKeyDown, false);
   1.964 +
   1.965 +    // Build the twisty expand/collapse
   1.966 +    this.matchedExpander = doc.createElementNS(HTML_NS, "div");
   1.967 +    this.matchedExpander.className = "expander theme-twisty";
   1.968 +    this.matchedExpander.addEventListener("click", this.onMatchedToggle, false);
   1.969 +    this.element.appendChild(this.matchedExpander);
   1.970 +
   1.971 +    this.focusElement = () => this.element.focus();
   1.972 +
   1.973 +    // Build the style name element
   1.974 +    this.nameNode = doc.createElementNS(HTML_NS, "div");
   1.975 +    this.nameNode.setAttribute("class", "property-name theme-fg-color5");
   1.976 +    // Reset its tabindex attribute otherwise, if an ellipsis is applied
   1.977 +    // it will be reachable via TABing
   1.978 +    this.nameNode.setAttribute("tabindex", "");
   1.979 +    this.nameNode.textContent = this.nameNode.title = this.name;
   1.980 +    // Make it hand over the focus to the container
   1.981 +    this.onFocus = () => this.element.focus();
   1.982 +    this.nameNode.addEventListener("click", this.onFocus, false);
   1.983 +    this.element.appendChild(this.nameNode);
   1.984 +
   1.985 +    // Build the style value element
   1.986 +    this.valueNode = doc.createElementNS(HTML_NS, "div");
   1.987 +    this.valueNode.setAttribute("class", "property-value theme-fg-color1");
   1.988 +    // Reset its tabindex attribute otherwise, if an ellipsis is applied
   1.989 +    // it will be reachable via TABing
   1.990 +    this.valueNode.setAttribute("tabindex", "");
   1.991 +    this.valueNode.setAttribute("dir", "ltr");
   1.992 +    // Make it hand over the focus to the container
   1.993 +    this.valueNode.addEventListener("click", this.onFocus, false);
   1.994 +    this.element.appendChild(this.valueNode);
   1.995 +
   1.996 +    return this.element;
   1.997 +  },
   1.998 +
   1.999 +  buildSelectorContainer: function PropertyView_buildSelectorContainer()
  1.1000 +  {
  1.1001 +    let doc = this.tree.styleDocument;
  1.1002 +    let element = doc.createElementNS(HTML_NS, "div");
  1.1003 +    element.setAttribute("class", this.propertyContentClassName);
  1.1004 +    this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div");
  1.1005 +    this.matchedSelectorsContainer.setAttribute("class", "matchedselectors");
  1.1006 +    element.appendChild(this.matchedSelectorsContainer);
  1.1007 +
  1.1008 +    return element;
  1.1009 +  },
  1.1010 +
  1.1011 +  /**
  1.1012 +   * Refresh the panel's CSS property value.
  1.1013 +   */
  1.1014 +  refresh: function PropertyView_refresh()
  1.1015 +  {
  1.1016 +    this.element.className = this.propertyHeaderClassName;
  1.1017 +    this.element.nextElementSibling.className = this.propertyContentClassName;
  1.1018 +
  1.1019 +    if (this.prevViewedElement != this.tree.viewedElement) {
  1.1020 +      this._matchedSelectorViews = null;
  1.1021 +      this.prevViewedElement = this.tree.viewedElement;
  1.1022 +    }
  1.1023 +
  1.1024 +    if (!this.tree.viewedElement || !this.visible) {
  1.1025 +      this.valueNode.textContent = this.valueNode.title = "";
  1.1026 +      this.matchedSelectorsContainer.parentNode.hidden = true;
  1.1027 +      this.matchedSelectorsContainer.textContent = "";
  1.1028 +      this.matchedExpander.removeAttribute("open");
  1.1029 +      return;
  1.1030 +    }
  1.1031 +
  1.1032 +    this.tree.numVisibleProperties++;
  1.1033 +
  1.1034 +    let outputParser = this.tree._outputParser;
  1.1035 +    let frag = outputParser.parseCssProperty(this.propertyInfo.name,
  1.1036 +      this.propertyInfo.value,
  1.1037 +      {
  1.1038 +        colorSwatchClass: "computedview-colorswatch",
  1.1039 +        urlClass: "theme-link"
  1.1040 +        // No need to use baseURI here as computed URIs are never relative.
  1.1041 +      });
  1.1042 +    this.valueNode.innerHTML = "";
  1.1043 +    this.valueNode.appendChild(frag);
  1.1044 +
  1.1045 +    this.refreshMatchedSelectors();
  1.1046 +  },
  1.1047 +
  1.1048 +  /**
  1.1049 +   * Refresh the panel matched rules.
  1.1050 +   */
  1.1051 +  refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
  1.1052 +  {
  1.1053 +    let hasMatchedSelectors = this.hasMatchedSelectors;
  1.1054 +    this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
  1.1055 +
  1.1056 +    if (hasMatchedSelectors) {
  1.1057 +      this.matchedExpander.classList.add("expandable");
  1.1058 +    } else {
  1.1059 +      this.matchedExpander.classList.remove("expandable");
  1.1060 +    }
  1.1061 +
  1.1062 +    if (this.matchedExpanded && hasMatchedSelectors) {
  1.1063 +      return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => {
  1.1064 +        if (!this.matchedExpanded) {
  1.1065 +          return;
  1.1066 +        }
  1.1067 +
  1.1068 +        this._matchedSelectorResponse = matched;
  1.1069 +        CssHtmlTree.processTemplate(this.templateMatchedSelectors,
  1.1070 +          this.matchedSelectorsContainer, this);
  1.1071 +        this.matchedExpander.setAttribute("open", "");
  1.1072 +        this.tree.styleInspector.inspector.emit("computed-view-property-expanded");
  1.1073 +      }).then(null, console.error);
  1.1074 +    } else {
  1.1075 +      this.matchedSelectorsContainer.innerHTML = "";
  1.1076 +      this.matchedExpander.removeAttribute("open");
  1.1077 +      this.tree.styleInspector.inspector.emit("computed-view-property-collapsed");
  1.1078 +      return promise.resolve(undefined);
  1.1079 +    }
  1.1080 +  },
  1.1081 +
  1.1082 +  get matchedSelectors()
  1.1083 +  {
  1.1084 +    return this._matchedSelectorResponse;
  1.1085 +  },
  1.1086 +
  1.1087 +  /**
  1.1088 +   * Provide access to the matched SelectorViews that we are currently
  1.1089 +   * displaying.
  1.1090 +   */
  1.1091 +  get matchedSelectorViews()
  1.1092 +  {
  1.1093 +    if (!this._matchedSelectorViews) {
  1.1094 +      this._matchedSelectorViews = [];
  1.1095 +      this._matchedSelectorResponse.forEach(
  1.1096 +        function matchedSelectorViews_convert(aSelectorInfo) {
  1.1097 +          this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
  1.1098 +        }, this);
  1.1099 +    }
  1.1100 +
  1.1101 +    return this._matchedSelectorViews;
  1.1102 +  },
  1.1103 +
  1.1104 +  /**
  1.1105 +   * Update all the selector source links to reflect whether we're linking to
  1.1106 +   * original sources (e.g. Sass files).
  1.1107 +   */
  1.1108 +  updateSourceLinks: function PropertyView_updateSourceLinks()
  1.1109 +  {
  1.1110 +    if (!this._matchedSelectorViews) {
  1.1111 +      return;
  1.1112 +    }
  1.1113 +    for (let view of this._matchedSelectorViews) {
  1.1114 +      view.updateSourceLink();
  1.1115 +    }
  1.1116 +  },
  1.1117 +
  1.1118 +  /**
  1.1119 +   * The action when a user expands matched selectors.
  1.1120 +   *
  1.1121 +   * @param {Event} aEvent Used to determine the class name of the targets click
  1.1122 +   * event.
  1.1123 +   */
  1.1124 +  onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
  1.1125 +  {
  1.1126 +    this.matchedExpanded = !this.matchedExpanded;
  1.1127 +    this.refreshMatchedSelectors();
  1.1128 +    aEvent.preventDefault();
  1.1129 +  },
  1.1130 +
  1.1131 +  /**
  1.1132 +   * The action when a user clicks on the MDN help link for a property.
  1.1133 +   */
  1.1134 +  mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
  1.1135 +  {
  1.1136 +    let inspector = this.tree.styleInspector.inspector;
  1.1137 +
  1.1138 +    if (inspector.target.tab) {
  1.1139 +      let browserWin = inspector.target.tab.ownerDocument.defaultView;
  1.1140 +      browserWin.openUILinkIn(this.link, "tab");
  1.1141 +    }
  1.1142 +    aEvent.preventDefault();
  1.1143 +  },
  1.1144 +
  1.1145 +  /**
  1.1146 +   * Destroy this property view, removing event listeners
  1.1147 +   */
  1.1148 +  destroy: function PropertyView_destroy() {
  1.1149 +    this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
  1.1150 +    this.element.removeEventListener("keydown", this.onKeyDown, false);
  1.1151 +    this.element = null;
  1.1152 +
  1.1153 +    this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false);
  1.1154 +    this.matchedExpander = null;
  1.1155 +
  1.1156 +    this.nameNode.removeEventListener("click", this.onFocus, false);
  1.1157 +    this.nameNode = null;
  1.1158 +
  1.1159 +    this.valueNode.removeEventListener("click", this.onFocus, false);
  1.1160 +    this.valueNode = null;
  1.1161 +  }
  1.1162 +};
  1.1163 +
  1.1164 +/**
  1.1165 + * A container to give us easy access to display data from a CssRule
  1.1166 + * @param CssHtmlTree aTree, the owning CssHtmlTree
  1.1167 + * @param aSelectorInfo
  1.1168 + */
  1.1169 +function SelectorView(aTree, aSelectorInfo)
  1.1170 +{
  1.1171 +  this.tree = aTree;
  1.1172 +  this.selectorInfo = aSelectorInfo;
  1.1173 +  this._cacheStatusNames();
  1.1174 +
  1.1175 +  this.updateSourceLink();
  1.1176 +}
  1.1177 +
  1.1178 +/**
  1.1179 + * Decode for cssInfo.rule.status
  1.1180 + * @see SelectorView.prototype._cacheStatusNames
  1.1181 + * @see CssLogic.STATUS
  1.1182 + */
  1.1183 +SelectorView.STATUS_NAMES = [
  1.1184 +  // "Parent Match", "Matched", "Best Match"
  1.1185 +];
  1.1186 +
  1.1187 +SelectorView.CLASS_NAMES = [
  1.1188 +  "parentmatch", "matched", "bestmatch"
  1.1189 +];
  1.1190 +
  1.1191 +SelectorView.prototype = {
  1.1192 +  /**
  1.1193 +   * Cache localized status names.
  1.1194 +   *
  1.1195 +   * These statuses are localized inside the styleinspector.properties string
  1.1196 +   * bundle.
  1.1197 +   * @see css-logic.js - the CssLogic.STATUS array.
  1.1198 +   *
  1.1199 +   * @return {void}
  1.1200 +   */
  1.1201 +  _cacheStatusNames: function SelectorView_cacheStatusNames()
  1.1202 +  {
  1.1203 +    if (SelectorView.STATUS_NAMES.length) {
  1.1204 +      return;
  1.1205 +    }
  1.1206 +
  1.1207 +    for (let status in CssLogic.STATUS) {
  1.1208 +      let i = CssLogic.STATUS[status];
  1.1209 +      if (i > CssLogic.STATUS.UNMATCHED) {
  1.1210 +        let value = CssHtmlTree.l10n("rule.status." + status);
  1.1211 +        // Replace normal spaces with non-breaking spaces
  1.1212 +        SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0');
  1.1213 +      }
  1.1214 +    }
  1.1215 +  },
  1.1216 +
  1.1217 +  /**
  1.1218 +   * A localized version of cssRule.status
  1.1219 +   */
  1.1220 +  get statusText()
  1.1221 +  {
  1.1222 +    return SelectorView.STATUS_NAMES[this.selectorInfo.status];
  1.1223 +  },
  1.1224 +
  1.1225 +  /**
  1.1226 +   * Get class name for selector depending on status
  1.1227 +   */
  1.1228 +  get statusClass()
  1.1229 +  {
  1.1230 +    return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
  1.1231 +  },
  1.1232 +
  1.1233 +  get href()
  1.1234 +  {
  1.1235 +    if (this._href) {
  1.1236 +      return this._href;
  1.1237 +    }
  1.1238 +    let sheet = this.selectorInfo.rule.parentStyleSheet;
  1.1239 +    this._href = sheet ? sheet.href : "#";
  1.1240 +    return this._href;
  1.1241 +  },
  1.1242 +
  1.1243 +  get sourceText()
  1.1244 +  {
  1.1245 +    return this.selectorInfo.sourceText;
  1.1246 +  },
  1.1247 +
  1.1248 +
  1.1249 +  get value()
  1.1250 +  {
  1.1251 +    return this.selectorInfo.value;
  1.1252 +  },
  1.1253 +
  1.1254 +  get outputFragment()
  1.1255 +  {
  1.1256 +    // Sadly, because this fragment is added to the template by DOM Templater
  1.1257 +    // we lose any events that are attached. This means that URLs will open in a
  1.1258 +    // new window. At some point we should fix this by stopping using the
  1.1259 +    // templater.
  1.1260 +    let outputParser = this.tree._outputParser;
  1.1261 +    let frag = outputParser.parseCssProperty(
  1.1262 +      this.selectorInfo.name,
  1.1263 +      this.selectorInfo.value, {
  1.1264 +      colorSwatchClass: "computedview-colorswatch",
  1.1265 +      urlClass: "theme-link",
  1.1266 +      baseURI: this.selectorInfo.rule.href
  1.1267 +    });
  1.1268 +    return frag;
  1.1269 +  },
  1.1270 +
  1.1271 +  /**
  1.1272 +   * Update the text of the source link to reflect whether we're showing
  1.1273 +   * original sources or not.
  1.1274 +   */
  1.1275 +  updateSourceLink: function()
  1.1276 +  {
  1.1277 +    this.updateSource().then((oldSource) => {
  1.1278 +      if (oldSource != this.source && this.tree.propertyContainer) {
  1.1279 +        let selector = '[sourcelocation="' + oldSource + '"]';
  1.1280 +        let link = this.tree.propertyContainer.querySelector(selector);
  1.1281 +        if (link) {
  1.1282 +          link.textContent = this.source;
  1.1283 +          link.setAttribute("sourcelocation", this.source);
  1.1284 +        }
  1.1285 +      }
  1.1286 +    });
  1.1287 +  },
  1.1288 +
  1.1289 +  /**
  1.1290 +   * Update the 'source' store based on our original sources preference.
  1.1291 +   */
  1.1292 +  updateSource: function()
  1.1293 +  {
  1.1294 +    let rule = this.selectorInfo.rule;
  1.1295 +    this.sheet = rule.parentStyleSheet;
  1.1296 +
  1.1297 +    if (!rule || !this.sheet) {
  1.1298 +      let oldSource = this.source;
  1.1299 +      this.source = CssLogic.l10n("rule.sourceElement");
  1.1300 +      this.href = "#";
  1.1301 +      return promise.resolve(oldSource);
  1.1302 +    }
  1.1303 +
  1.1304 +    let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
  1.1305 +
  1.1306 +    if (showOrig && rule.type != ELEMENT_STYLE) {
  1.1307 +      let deferred = promise.defer();
  1.1308 +
  1.1309 +      // set as this first so we show something while we're fetching
  1.1310 +      this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
  1.1311 +
  1.1312 +      rule.getOriginalLocation().then(({href, line, column}) => {
  1.1313 +        let oldSource = this.source;
  1.1314 +        this.source = CssLogic.shortSource({href: href}) + ":" + line;
  1.1315 +        deferred.resolve(oldSource);
  1.1316 +      });
  1.1317 +
  1.1318 +      return deferred.promise;
  1.1319 +    }
  1.1320 +
  1.1321 +    let oldSource = this.source;
  1.1322 +    this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
  1.1323 +    return promise.resolve(oldSource);
  1.1324 +  },
  1.1325 +
  1.1326 +  /**
  1.1327 +   * Open the style editor if the RETURN key was pressed.
  1.1328 +   */
  1.1329 +  maybeOpenStyleEditor: function(aEvent)
  1.1330 +  {
  1.1331 +    let keyEvent = Ci.nsIDOMKeyEvent;
  1.1332 +    if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
  1.1333 +      this.openStyleEditor();
  1.1334 +    }
  1.1335 +  },
  1.1336 +
  1.1337 +  /**
  1.1338 +   * When a css link is clicked this method is called in order to either:
  1.1339 +   *   1. Open the link in view source (for chrome stylesheets).
  1.1340 +   *   2. Open the link in the style editor.
  1.1341 +   *
  1.1342 +   *   We can only view stylesheets contained in document.styleSheets inside the
  1.1343 +   *   style editor.
  1.1344 +   *
  1.1345 +   * @param aEvent The click event
  1.1346 +   */
  1.1347 +  openStyleEditor: function(aEvent)
  1.1348 +  {
  1.1349 +    let inspector = this.tree.styleInspector.inspector;
  1.1350 +    let rule = this.selectorInfo.rule;
  1.1351 +
  1.1352 +    // The style editor can only display stylesheets coming from content because
  1.1353 +    // chrome stylesheets are not listed in the editor's stylesheet selector.
  1.1354 +    //
  1.1355 +    // If the stylesheet is a content stylesheet we send it to the style
  1.1356 +    // editor else we display it in the view source window.
  1.1357 +    let sheet = rule.parentStyleSheet;
  1.1358 +    if (!sheet || sheet.isSystem) {
  1.1359 +      let contentDoc = null;
  1.1360 +      if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
  1.1361 +        let rawNode = this.tree.viewedElement.rawNode();
  1.1362 +        if (rawNode) {
  1.1363 +          contentDoc = rawNode.ownerDocument;
  1.1364 +        }
  1.1365 +      }
  1.1366 +      let viewSourceUtils = inspector.viewSourceUtils;
  1.1367 +      viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line);
  1.1368 +      return;
  1.1369 +    }
  1.1370 +
  1.1371 +    let location = promise.resolve({
  1.1372 +      href: rule.href,
  1.1373 +      line: rule.line
  1.1374 +    });
  1.1375 +    if (rule.href && Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
  1.1376 +      location = rule.getOriginalLocation();
  1.1377 +    }
  1.1378 +
  1.1379 +    location.then(({href, line}) => {
  1.1380 +      let target = inspector.target;
  1.1381 +      if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
  1.1382 +        gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
  1.1383 +          toolbox.getCurrentPanel().selectStyleSheet(href, line);
  1.1384 +        });
  1.1385 +      }
  1.1386 +    });
  1.1387 +  }
  1.1388 +};
  1.1389 +
  1.1390 +exports.CssHtmlTree = CssHtmlTree;
  1.1391 +exports.PropertyView = PropertyView;

mercurial