browser/devtools/styleinspector/computed-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 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 const {Cc, Ci, Cu} = require("chrome");
     9 const ToolDefinitions = require("main").Tools;
    10 const {CssLogic} = require("devtools/styleinspector/css-logic");
    11 const {ELEMENT_STYLE} = require("devtools/server/actors/styles");
    12 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    13 const {EventEmitter} = require("devtools/toolkit/event-emitter");
    14 const {OutputParser} = require("devtools/output-parser");
    15 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
    16 const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
    17 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
    19 Cu.import("resource://gre/modules/Services.jsm");
    20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    21 Cu.import("resource://gre/modules/devtools/Templater.jsm");
    23 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    24                                   "resource://gre/modules/PluralForm.jsm");
    26 const FILTER_CHANGED_TIMEOUT = 300;
    27 const HTML_NS = "http://www.w3.org/1999/xhtml";
    28 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    30 /**
    31  * Helper for long-running processes that should yield occasionally to
    32  * the mainloop.
    33  *
    34  * @param {Window} aWin
    35  *        Timeouts will be set on this window when appropriate.
    36  * @param {Generator} aGenerator
    37  *        Will iterate this generator.
    38  * @param {object} aOptions
    39  *        Options for the update process:
    40  *          onItem {function} Will be called with the value of each iteration.
    41  *          onBatch {function} Will be called after each batch of iterations,
    42  *            before yielding to the main loop.
    43  *          onDone {function} Will be called when iteration is complete.
    44  *          onCancel {function} Will be called if the process is canceled.
    45  *          threshold {int} How long to process before yielding, in ms.
    46  *
    47  * @constructor
    48  */
    49 function UpdateProcess(aWin, aGenerator, aOptions)
    50 {
    51   this.win = aWin;
    52   this.iter = _Iterator(aGenerator);
    53   this.onItem = aOptions.onItem || function() {};
    54   this.onBatch = aOptions.onBatch || function () {};
    55   this.onDone = aOptions.onDone || function() {};
    56   this.onCancel = aOptions.onCancel || function() {};
    57   this.threshold = aOptions.threshold || 45;
    59   this.canceled = false;
    60 }
    62 UpdateProcess.prototype = {
    63   /**
    64    * Schedule a new batch on the main loop.
    65    */
    66   schedule: function UP_schedule()
    67   {
    68     if (this.canceled) {
    69       return;
    70     }
    71     this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0);
    72   },
    74   /**
    75    * Cancel the running process.  onItem will not be called again,
    76    * and onCancel will be called.
    77    */
    78   cancel: function UP_cancel()
    79   {
    80     if (this._timeout) {
    81       this.win.clearTimeout(this._timeout);
    82       this._timeout = 0;
    83     }
    84     this.canceled = true;
    85     this.onCancel();
    86   },
    88   _timeoutHandler: function UP_timeoutHandler() {
    89     this._timeout = null;
    90     try {
    91       this._runBatch();
    92       this.schedule();
    93     } catch(e) {
    94       if (e instanceof StopIteration) {
    95         this.onBatch();
    96         this.onDone();
    97         return;
    98       }
    99       console.error(e);
   100       throw e;
   101     }
   102   },
   104   _runBatch: function Y_runBatch()
   105   {
   106     let time = Date.now();
   107     while(!this.canceled) {
   108       // Continue until iter.next() throws...
   109       let next = this.iter.next();
   110       this.onItem(next[1]);
   111       if ((Date.now() - time) > this.threshold) {
   112         this.onBatch();
   113         return;
   114       }
   115     }
   116   }
   117 };
   119 /**
   120  * CssHtmlTree is a panel that manages the display of a table sorted by style.
   121  * There should be one instance of CssHtmlTree per style display (of which there
   122  * will generally only be one).
   123  *
   124  * @params {StyleInspector} aStyleInspector The owner of this CssHtmlTree
   125  * @param {PageStyleFront} aPageStyle
   126  *        Front for the page style actor that will be providing
   127  *        the style information.
   128  *
   129  * @constructor
   130  */
   131 function CssHtmlTree(aStyleInspector, aPageStyle)
   132 {
   133   this.styleWindow = aStyleInspector.window;
   134   this.styleDocument = aStyleInspector.window.document;
   135   this.styleInspector = aStyleInspector;
   136   this.pageStyle = aPageStyle;
   137   this.propertyViews = [];
   139   this._outputParser = new OutputParser();
   141   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
   142     getService(Ci.nsIXULChromeRegistry);
   143   this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
   145   // Create bound methods.
   146   this.focusWindow = this.focusWindow.bind(this);
   147   this._onContextMenu = this._onContextMenu.bind(this);
   148   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   149   this._onSelectAll = this._onSelectAll.bind(this);
   150   this._onClick = this._onClick.bind(this);
   151   this._onCopy = this._onCopy.bind(this);
   153   this.styleDocument.addEventListener("copy", this._onCopy);
   154   this.styleDocument.addEventListener("mousedown", this.focusWindow);
   155   this.styleDocument.addEventListener("contextmenu", this._onContextMenu);
   157   // Nodes used in templating
   158   this.root = this.styleDocument.getElementById("root");
   159   this.templateRoot = this.styleDocument.getElementById("templateRoot");
   160   this.propertyContainer = this.styleDocument.getElementById("propertyContainer");
   162   // Listen for click events
   163   this.propertyContainer.addEventListener("click", this._onClick, false);
   165   // No results text.
   166   this.noResults = this.styleDocument.getElementById("noResults");
   168   // Refresh panel when color unit changed.
   169   this._handlePrefChange = this._handlePrefChange.bind(this);
   170   gDevTools.on("pref-changed", this._handlePrefChange);
   172   // Refresh panel when pref for showing original sources changes
   173   this._updateSourceLinks = this._updateSourceLinks.bind(this);
   174   this._prefObserver = new PrefObserver("devtools.");
   175   this._prefObserver.on(PREF_ORIG_SOURCES, this._updateSourceLinks);
   177   CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
   179   // The element that we're inspecting, and the document that it comes from.
   180   this.viewedElement = null;
   182   // Properties preview tooltip
   183   this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc);
   184   this.tooltip.startTogglingOnHover(this.propertyContainer,
   185     this._onTooltipTargetHover.bind(this));
   187   this._buildContextMenu();
   188   this.createStyleViews();
   189 }
   191 /**
   192  * Memoized lookup of a l10n string from a string bundle.
   193  * @param {string} aName The key to lookup.
   194  * @returns A localized version of the given key.
   195  */
   196 CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
   197 {
   198   try {
   199     return CssHtmlTree._strings.GetStringFromName(aName);
   200   } catch (ex) {
   201     Services.console.logStringMessage("Error reading '" + aName + "'");
   202     throw new Error("l10n error with " + aName);
   203   }
   204 };
   206 /**
   207  * Clone the given template node, and process it by resolving ${} references
   208  * in the template.
   209  *
   210  * @param {nsIDOMElement} aTemplate the template note to use.
   211  * @param {nsIDOMElement} aDestination the destination node where the
   212  * processed nodes will be displayed.
   213  * @param {object} aData the data to pass to the template.
   214  * @param {Boolean} aPreserveDestination If true then the template will be
   215  * appended to aDestination's content else aDestination.innerHTML will be
   216  * cleared before the template is appended.
   217  */
   218 CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
   219                                   aDestination, aData, aPreserveDestination)
   220 {
   221   if (!aPreserveDestination) {
   222     aDestination.innerHTML = "";
   223   }
   225   // All the templater does is to populate a given DOM tree with the given
   226   // values, so we need to clone the template first.
   227   let duplicated = aTemplate.cloneNode(true);
   229   // See https://github.com/mozilla/domtemplate/blob/master/README.md
   230   // for docs on the template() function
   231   template(duplicated, aData, { allowEval: true });
   232   while (duplicated.firstChild) {
   233     aDestination.appendChild(duplicated.firstChild);
   234   }
   235 };
   237 XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
   238         .createBundle("chrome://global/locale/devtools/styleinspector.properties"));
   240 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
   241   return Cc["@mozilla.org/widget/clipboardhelper;1"].
   242     getService(Ci.nsIClipboardHelper);
   243 });
   245 CssHtmlTree.prototype = {
   246   // Cache the list of properties that match the selected element.
   247   _matchedProperties: null,
   249   // Used for cancelling timeouts in the style filter.
   250   _filterChangedTimeout: null,
   252   // The search filter
   253   searchField: null,
   255   // Reference to the "Include browser styles" checkbox.
   256   includeBrowserStylesCheckbox: null,
   258   // Holds the ID of the panelRefresh timeout.
   259   _panelRefreshTimeout: null,
   261   // Toggle for zebra striping
   262   _darkStripe: true,
   264   // Number of visible properties
   265   numVisibleProperties: 0,
   267   setPageStyle: function(pageStyle) {
   268     this.pageStyle = pageStyle;
   269   },
   271   get includeBrowserStyles()
   272   {
   273     return this.includeBrowserStylesCheckbox.checked;
   274   },
   276   _handlePrefChange: function(event, data) {
   277     if (this._computed && (data.pref == "devtools.defaultColorUnit" ||
   278         data.pref == PREF_ORIG_SOURCES)) {
   279       this.refreshPanel();
   280     }
   281   },
   283   /**
   284    * Update the highlighted element. The CssHtmlTree panel will show the style
   285    * information for the given element.
   286    * @param {nsIDOMElement} aElement The highlighted node to get styles for.
   287    *
   288    * @returns a promise that will be resolved when highlighting is complete.
   289    */
   290   highlight: function(aElement) {
   291     if (!aElement) {
   292       this.viewedElement = null;
   293       this.noResults.hidden = false;
   295       if (this._refreshProcess) {
   296         this._refreshProcess.cancel();
   297       }
   298       // Hiding all properties
   299       for (let propView of this.propertyViews) {
   300         propView.refresh();
   301       }
   302       return promise.resolve(undefined);
   303     }
   305     this.tooltip.hide();
   307     if (aElement === this.viewedElement) {
   308       return promise.resolve(undefined);
   309     }
   311     this.viewedElement = aElement;
   312     this.refreshSourceFilter();
   314     return this.refreshPanel();
   315   },
   317   _createPropertyViews: function()
   318   {
   319     if (this._createViewsPromise) {
   320       return this._createViewsPromise;
   321     }
   323     let deferred = promise.defer();
   324     this._createViewsPromise = deferred.promise;
   326     this.refreshSourceFilter();
   327     this.numVisibleProperties = 0;
   328     let fragment = this.styleDocument.createDocumentFragment();
   330     this._createViewsProcess = new UpdateProcess(this.styleWindow, CssHtmlTree.propertyNames, {
   331       onItem: (aPropertyName) => {
   332         // Per-item callback.
   333         let propView = new PropertyView(this, aPropertyName);
   334         fragment.appendChild(propView.buildMain());
   335         fragment.appendChild(propView.buildSelectorContainer());
   337         if (propView.visible) {
   338           this.numVisibleProperties++;
   339         }
   340         this.propertyViews.push(propView);
   341       },
   342       onCancel: () => {
   343         deferred.reject("_createPropertyViews cancelled");
   344       },
   345       onDone: () => {
   346         // Completed callback.
   347         this.propertyContainer.appendChild(fragment);
   348         this.noResults.hidden = this.numVisibleProperties > 0;
   349         deferred.resolve(undefined);
   350       }
   351     });
   353     this._createViewsProcess.schedule();
   354     return deferred.promise;
   355   },
   357   /**
   358    * Refresh the panel content.
   359    */
   360   refreshPanel: function CssHtmlTree_refreshPanel()
   361   {
   362     if (!this.viewedElement) {
   363       return promise.resolve();
   364     }
   366     return promise.all([
   367       this._createPropertyViews(),
   368       this.pageStyle.getComputed(this.viewedElement, {
   369         filter: this._sourceFilter,
   370         onlyMatched: !this.includeBrowserStyles,
   371         markMatched: true
   372       })
   373     ]).then(([createViews, computed]) => {
   374       this._matchedProperties = new Set;
   375       for (let name in computed) {
   376         if (computed[name].matched) {
   377           this._matchedProperties.add(name);
   378         }
   379       }
   380       this._computed = computed;
   382       if (this._refreshProcess) {
   383         this._refreshProcess.cancel();
   384       }
   386       this.noResults.hidden = true;
   388       // Reset visible property count
   389       this.numVisibleProperties = 0;
   391       // Reset zebra striping.
   392       this._darkStripe = true;
   394       let deferred = promise.defer();
   395       this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
   396         onItem: (aPropView) => {
   397           aPropView.refresh();
   398         },
   399         onDone: () => {
   400           this._refreshProcess = null;
   401           this.noResults.hidden = this.numVisibleProperties > 0;
   402           this.styleInspector.inspector.emit("computed-view-refreshed");
   403           deferred.resolve(undefined);
   404         }
   405       });
   406       this._refreshProcess.schedule();
   407       return deferred.promise;
   408     }).then(null, (err) => console.error(err));
   409   },
   411   /**
   412    * Called when the user enters a search term.
   413    *
   414    * @param {Event} aEvent the DOM Event object.
   415    */
   416   filterChanged: function CssHtmlTree_filterChanged(aEvent)
   417   {
   418     let win = this.styleWindow;
   420     if (this._filterChangedTimeout) {
   421       win.clearTimeout(this._filterChangedTimeout);
   422     }
   424     this._filterChangedTimeout = win.setTimeout(function() {
   425       this.refreshPanel();
   426       this._filterChangeTimeout = null;
   427     }.bind(this), FILTER_CHANGED_TIMEOUT);
   428   },
   430   /**
   431    * The change event handler for the includeBrowserStyles checkbox.
   432    *
   433    * @param {Event} aEvent the DOM Event object.
   434    */
   435   includeBrowserStylesChanged:
   436   function CssHtmltree_includeBrowserStylesChanged(aEvent)
   437   {
   438     this.refreshSourceFilter();
   439     this.refreshPanel();
   440   },
   442   /**
   443    * When includeBrowserStyles.checked is false we only display properties that
   444    * have matched selectors and have been included by the document or one of the
   445    * document's stylesheets. If .checked is false we display all properties
   446    * including those that come from UA stylesheets.
   447    */
   448   refreshSourceFilter: function CssHtmlTree_setSourceFilter()
   449   {
   450     this._matchedProperties = null;
   451     this._sourceFilter = this.includeBrowserStyles ?
   452                                  CssLogic.FILTER.UA :
   453                                  CssLogic.FILTER.USER;
   454   },
   456   _updateSourceLinks: function CssHtmlTree__updateSourceLinks()
   457   {
   458     for (let propView of this.propertyViews) {
   459       propView.updateSourceLinks();
   460     }
   461   },
   463   /**
   464    * The CSS as displayed by the UI.
   465    */
   466   createStyleViews: function CssHtmlTree_createStyleViews()
   467   {
   468     if (CssHtmlTree.propertyNames) {
   469       return;
   470     }
   472     CssHtmlTree.propertyNames = [];
   474     // Here we build and cache a list of css properties supported by the browser
   475     // We could use any element but let's use the main document's root element
   476     let styles = this.styleWindow.getComputedStyle(this.styleDocument.documentElement);
   477     let mozProps = [];
   478     for (let i = 0, numStyles = styles.length; i < numStyles; i++) {
   479       let prop = styles.item(i);
   480       if (prop.charAt(0) == "-") {
   481         mozProps.push(prop);
   482       } else {
   483         CssHtmlTree.propertyNames.push(prop);
   484       }
   485     }
   487     CssHtmlTree.propertyNames.sort();
   488     CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames,
   489       mozProps.sort());
   491     this._createPropertyViews();
   492   },
   494   /**
   495    * Get a set of properties that have matched selectors.
   496    *
   497    * @return {Set} If a property name is in the set, it has matching selectors.
   498    */
   499   get matchedProperties()
   500   {
   501     return this._matchedProperties || new Set;
   502   },
   504   /**
   505    * Focus the window on mousedown.
   506    *
   507    * @param aEvent The event object
   508    */
   509   focusWindow: function(aEvent)
   510   {
   511     let win = this.styleDocument.defaultView;
   512     win.focus();
   513   },
   515   /**
   516    * Executed by the tooltip when the pointer hovers over an element of the view.
   517    * Used to decide whether the tooltip should be shown or not and to actually
   518    * put content in it.
   519    * Checks if the hovered target is a css value we support tooltips for.
   520    */
   521   _onTooltipTargetHover: function(target)
   522   {
   523     let inspector = this.styleInspector.inspector;
   525     // Test for image url
   526     if (target.classList.contains("theme-link") && inspector.hasUrlToImageDataResolver) {
   527       let propValue = target.parentNode;
   528       let propName = propValue.parentNode.querySelector(".property-name");
   529       if (propName.textContent === "background-image") {
   530         let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
   531         let uri = CssLogic.getBackgroundImageUriFromProperty(propValue.textContent);
   532         return this.tooltip.setRelativeImageContent(uri, inspector.inspector, maxDim);
   533       }
   534     }
   536     if (target.classList.contains("property-value")) {
   537       let propValue = target;
   538       let propName = target.parentNode.querySelector(".property-name");
   540       // Test for css transform
   541       if (propName.textContent === "transform") {
   542         return this.tooltip.setCssTransformContent(propValue.textContent,
   543           this.pageStyle, this.viewedElement);
   544       }
   546       // Test for font family
   547       if (propName.textContent === "font-family") {
   548         this.tooltip.setFontFamilyContent(propValue.textContent);
   549         return true;
   550       }
   551     }
   553     // If the target isn't one that should receive a tooltip, signal it by rejecting
   554     // a promise
   555     return promise.reject();
   556   },
   558   /**
   559    * Create a context menu.
   560    */
   561   _buildContextMenu: function()
   562   {
   563     let doc = this.styleDocument.defaultView.parent.document;
   565     this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup");
   566     this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
   567     this._contextmenu.id = "computed-view-context-menu";
   569     // Select All
   570     this.menuitemSelectAll = createMenuItem(this._contextmenu, {
   571       label: "computedView.contextmenu.selectAll",
   572       accesskey: "computedView.contextmenu.selectAll.accessKey",
   573       command: this._onSelectAll
   574     });
   576     // Copy
   577     this.menuitemCopy = createMenuItem(this._contextmenu, {
   578       label: "computedView.contextmenu.copy",
   579       accesskey: "computedView.contextmenu.copy.accessKey",
   580       command: this._onCopy
   581     });
   583     // Show Original Sources
   584     this.menuitemSources= createMenuItem(this._contextmenu, {
   585       label: "ruleView.contextmenu.showOrigSources",
   586       accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
   587       command: this._onToggleOrigSources
   588     });
   590     let popupset = doc.documentElement.querySelector("popupset");
   591     if (!popupset) {
   592       popupset = doc.createElementNS(XUL_NS, "popupset");
   593       doc.documentElement.appendChild(popupset);
   594     }
   595     popupset.appendChild(this._contextmenu);
   596   },
   598   /**
   599    * Update the context menu. This means enabling or disabling menuitems as
   600    * appropriate.
   601    */
   602   _contextMenuUpdate: function()
   603   {
   604     let win = this.styleDocument.defaultView;
   605     let disable = win.getSelection().isCollapsed;
   606     this.menuitemCopy.disabled = disable;
   608     let label = "ruleView.contextmenu.showOrigSources";
   609     if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
   610       label = "ruleView.contextmenu.showCSSSources";
   611     }
   612     this.menuitemSources.setAttribute("label",
   613                                       CssHtmlTree.l10n(label));
   615     let accessKey = label + ".accessKey";
   616     this.menuitemSources.setAttribute("accesskey",
   617                                       CssHtmlTree.l10n(accessKey));
   618   },
   620   /**
   621    * Context menu handler.
   622    */
   623   _onContextMenu: function(event) {
   624     try {
   625       this.styleDocument.defaultView.focus();
   626       this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
   627     } catch(e) {
   628       console.error(e);
   629     }
   630   },
   632   /**
   633    * Select all text.
   634    */
   635   _onSelectAll: function()
   636   {
   637     try {
   638       let win = this.styleDocument.defaultView;
   639       let selection = win.getSelection();
   641       selection.selectAllChildren(this.styleDocument.documentElement);
   642     } catch(e) {
   643       console.error(e);
   644     }
   645   },
   647   _onClick: function(event) {
   648     let target = event.target;
   650     if (target.nodeName === "a") {
   651       event.stopPropagation();
   652       event.preventDefault();
   653       let browserWin = this.styleInspector.inspector.target
   654                            .tab.ownerDocument.defaultView;
   655       browserWin.openUILinkIn(target.href, "tab");
   656     }
   657   },
   659   /**
   660    * Copy selected text.
   661    *
   662    * @param event The event object
   663    */
   664   _onCopy: function(event)
   665   {
   666     try {
   667       let win = this.styleDocument.defaultView;
   668       let text = win.getSelection().toString().trim();
   670       // Tidy up block headings by moving CSS property names and their values onto
   671       // the same line and inserting a colon between them.
   672       let textArray = text.split(/[\r\n]+/);
   673       let result = "";
   675       // Parse text array to output string.
   676       if (textArray.length > 1) {
   677         for (let prop of textArray) {
   678           if (CssHtmlTree.propertyNames.indexOf(prop) !== -1) {
   679             // Property name
   680             result += prop;
   681           } else {
   682             // Property value
   683             result += ": " + prop;
   684             if (result.length > 0) {
   685               result += ";\n";
   686             }
   687           }
   688         }
   689       } else {
   690         // Short text fragment.
   691         result = textArray[0];
   692       }
   694       clipboardHelper.copyString(result, this.styleDocument);
   696       if (event) {
   697         event.preventDefault();
   698       }
   699     } catch(e) {
   700       console.error(e);
   701     }
   702   },
   704   /**
   705    *  Toggle the original sources pref.
   706    */
   707   _onToggleOrigSources: function()
   708   {
   709     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
   710     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   711   },
   713   /**
   714    * Destructor for CssHtmlTree.
   715    */
   716   destroy: function CssHtmlTree_destroy()
   717   {
   718     delete this.viewedElement;
   719     delete this._outputParser;
   721     // Remove event listeners
   722     this.includeBrowserStylesCheckbox.removeEventListener("command",
   723       this.includeBrowserStylesChanged);
   724     this.searchField.removeEventListener("command", this.filterChanged);
   725     gDevTools.off("pref-changed", this._handlePrefChange);
   727     this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks);
   728     this._prefObserver.destroy();
   730     // Cancel tree construction
   731     if (this._createViewsProcess) {
   732       this._createViewsProcess.cancel();
   733     }
   734     if (this._refreshProcess) {
   735       this._refreshProcess.cancel();
   736     }
   738     this.propertyContainer.removeEventListener("click", this._onClick, false);
   740     // Remove context menu
   741     if (this._contextmenu) {
   742       // Destroy the Select All menuitem.
   743       this.menuitemCopy.removeEventListener("command", this._onCopy);
   744       this.menuitemCopy = null;
   746       // Destroy the Copy menuitem.
   747       this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
   748       this.menuitemSelectAll = null;
   750       // Destroy the context menu.
   751       this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
   752       this._contextmenu.parentNode.removeChild(this._contextmenu);
   753       this._contextmenu = null;
   754     }
   756     this.tooltip.stopTogglingOnHover(this.propertyContainer);
   757     this.tooltip.destroy();
   759     // Remove bound listeners
   760     this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
   761     this.styleDocument.removeEventListener("copy", this._onCopy);
   762     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
   764     // Nodes used in templating
   765     delete this.root;
   766     delete this.propertyContainer;
   767     delete this.panel;
   769     // The document in which we display the results (csshtmltree.xul).
   770     delete this.styleDocument;
   772     for (let propView of this.propertyViews)  {
   773       propView.destroy();
   774     }
   776     // The element that we're inspecting, and the document that it comes from.
   777     delete this.propertyViews;
   778     delete this.styleWindow;
   779     delete this.styleDocument;
   780     delete this.styleInspector;
   781   }
   782 };
   784 function PropertyInfo(aTree, aName) {
   785   this.tree = aTree;
   786   this.name = aName;
   787 }
   788 PropertyInfo.prototype = {
   789   get value() {
   790     if (this.tree._computed) {
   791       let value = this.tree._computed[this.name].value;
   792       return value;
   793     }
   794   }
   795 };
   797 function createMenuItem(aMenu, aAttributes)
   798 {
   799   let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
   801   item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label));
   802   item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey));
   803   item.addEventListener("command", aAttributes.command);
   805   aMenu.appendChild(item);
   807   return item;
   808 }
   810 /**
   811  * A container to give easy access to property data from the template engine.
   812  *
   813  * @constructor
   814  * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with.
   815  * @param {string} aName the CSS property name for which this PropertyView
   816  * instance will render the rules.
   817  */
   818 function PropertyView(aTree, aName)
   819 {
   820   this.tree = aTree;
   821   this.name = aName;
   822   this.getRTLAttr = aTree.getRTLAttr;
   824   this.link = "https://developer.mozilla.org/CSS/" + aName;
   826   this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors");
   827   this._propertyInfo = new PropertyInfo(aTree, aName);
   828 }
   830 PropertyView.prototype = {
   831   // The parent element which contains the open attribute
   832   element: null,
   834   // Property header node
   835   propertyHeader: null,
   837   // Destination for property names
   838   nameNode: null,
   840   // Destination for property values
   841   valueNode: null,
   843   // Are matched rules expanded?
   844   matchedExpanded: false,
   846   // Matched selector container
   847   matchedSelectorsContainer: null,
   849   // Matched selector expando
   850   matchedExpander: null,
   852   // Cache for matched selector views
   853   _matchedSelectorViews: null,
   855   // The previously selected element used for the selector view caches
   856   prevViewedElement: null,
   858   /**
   859    * Get the computed style for the current property.
   860    *
   861    * @return {string} the computed style for the current property of the
   862    * currently highlighted element.
   863    */
   864   get value()
   865   {
   866     return this.propertyInfo.value;
   867   },
   869   /**
   870    * An easy way to access the CssPropertyInfo behind this PropertyView.
   871    */
   872   get propertyInfo()
   873   {
   874     return this._propertyInfo;
   875   },
   877   /**
   878    * Does the property have any matched selectors?
   879    */
   880   get hasMatchedSelectors()
   881   {
   882     return this.tree.matchedProperties.has(this.name);
   883   },
   885   /**
   886    * Should this property be visible?
   887    */
   888   get visible()
   889   {
   890     if (!this.tree.viewedElement) {
   891       return false;
   892     }
   894     if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
   895       return false;
   896     }
   898     let searchTerm = this.tree.searchField.value.toLowerCase();
   899     if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 &&
   900       this.value.toLowerCase().indexOf(searchTerm) == -1) {
   901       return false;
   902     }
   904     return true;
   905   },
   907   /**
   908    * Returns the className that should be assigned to the propertyView.
   909    * @return string
   910    */
   911   get propertyHeaderClassName()
   912   {
   913     if (this.visible) {
   914       let isDark = this.tree._darkStripe = !this.tree._darkStripe;
   915       return isDark ? "property-view theme-bg-darker" : "property-view";
   916     }
   917     return "property-view-hidden";
   918   },
   920   /**
   921    * Returns the className that should be assigned to the propertyView content
   922    * container.
   923    * @return string
   924    */
   925   get propertyContentClassName()
   926   {
   927     if (this.visible) {
   928       let isDark = this.tree._darkStripe;
   929       return isDark ? "property-content theme-bg-darker" : "property-content";
   930     }
   931     return "property-content-hidden";
   932   },
   934   /**
   935    * Build the markup for on computed style
   936    * @return Element
   937    */
   938   buildMain: function PropertyView_buildMain()
   939   {
   940     let doc = this.tree.styleDocument;
   942     // Build the container element
   943     this.onMatchedToggle = this.onMatchedToggle.bind(this);
   944     this.element = doc.createElementNS(HTML_NS, "div");
   945     this.element.setAttribute("class", this.propertyHeaderClassName);
   946     this.element.addEventListener("dblclick", this.onMatchedToggle, false);
   948     // Make it keyboard navigable
   949     this.element.setAttribute("tabindex", "0");
   950     this.onKeyDown = (aEvent) => {
   951       let keyEvent = Ci.nsIDOMKeyEvent;
   952       if (aEvent.keyCode == keyEvent.DOM_VK_F1) {
   953         this.mdnLinkClick();
   954       }
   955       if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
   956         aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
   957         this.onMatchedToggle(aEvent);
   958       }
   959     };
   960     this.element.addEventListener("keydown", this.onKeyDown, false);
   962     // Build the twisty expand/collapse
   963     this.matchedExpander = doc.createElementNS(HTML_NS, "div");
   964     this.matchedExpander.className = "expander theme-twisty";
   965     this.matchedExpander.addEventListener("click", this.onMatchedToggle, false);
   966     this.element.appendChild(this.matchedExpander);
   968     this.focusElement = () => this.element.focus();
   970     // Build the style name element
   971     this.nameNode = doc.createElementNS(HTML_NS, "div");
   972     this.nameNode.setAttribute("class", "property-name theme-fg-color5");
   973     // Reset its tabindex attribute otherwise, if an ellipsis is applied
   974     // it will be reachable via TABing
   975     this.nameNode.setAttribute("tabindex", "");
   976     this.nameNode.textContent = this.nameNode.title = this.name;
   977     // Make it hand over the focus to the container
   978     this.onFocus = () => this.element.focus();
   979     this.nameNode.addEventListener("click", this.onFocus, false);
   980     this.element.appendChild(this.nameNode);
   982     // Build the style value element
   983     this.valueNode = doc.createElementNS(HTML_NS, "div");
   984     this.valueNode.setAttribute("class", "property-value theme-fg-color1");
   985     // Reset its tabindex attribute otherwise, if an ellipsis is applied
   986     // it will be reachable via TABing
   987     this.valueNode.setAttribute("tabindex", "");
   988     this.valueNode.setAttribute("dir", "ltr");
   989     // Make it hand over the focus to the container
   990     this.valueNode.addEventListener("click", this.onFocus, false);
   991     this.element.appendChild(this.valueNode);
   993     return this.element;
   994   },
   996   buildSelectorContainer: function PropertyView_buildSelectorContainer()
   997   {
   998     let doc = this.tree.styleDocument;
   999     let element = doc.createElementNS(HTML_NS, "div");
  1000     element.setAttribute("class", this.propertyContentClassName);
  1001     this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div");
  1002     this.matchedSelectorsContainer.setAttribute("class", "matchedselectors");
  1003     element.appendChild(this.matchedSelectorsContainer);
  1005     return element;
  1006   },
  1008   /**
  1009    * Refresh the panel's CSS property value.
  1010    */
  1011   refresh: function PropertyView_refresh()
  1013     this.element.className = this.propertyHeaderClassName;
  1014     this.element.nextElementSibling.className = this.propertyContentClassName;
  1016     if (this.prevViewedElement != this.tree.viewedElement) {
  1017       this._matchedSelectorViews = null;
  1018       this.prevViewedElement = this.tree.viewedElement;
  1021     if (!this.tree.viewedElement || !this.visible) {
  1022       this.valueNode.textContent = this.valueNode.title = "";
  1023       this.matchedSelectorsContainer.parentNode.hidden = true;
  1024       this.matchedSelectorsContainer.textContent = "";
  1025       this.matchedExpander.removeAttribute("open");
  1026       return;
  1029     this.tree.numVisibleProperties++;
  1031     let outputParser = this.tree._outputParser;
  1032     let frag = outputParser.parseCssProperty(this.propertyInfo.name,
  1033       this.propertyInfo.value,
  1035         colorSwatchClass: "computedview-colorswatch",
  1036         urlClass: "theme-link"
  1037         // No need to use baseURI here as computed URIs are never relative.
  1038       });
  1039     this.valueNode.innerHTML = "";
  1040     this.valueNode.appendChild(frag);
  1042     this.refreshMatchedSelectors();
  1043   },
  1045   /**
  1046    * Refresh the panel matched rules.
  1047    */
  1048   refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
  1050     let hasMatchedSelectors = this.hasMatchedSelectors;
  1051     this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
  1053     if (hasMatchedSelectors) {
  1054       this.matchedExpander.classList.add("expandable");
  1055     } else {
  1056       this.matchedExpander.classList.remove("expandable");
  1059     if (this.matchedExpanded && hasMatchedSelectors) {
  1060       return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => {
  1061         if (!this.matchedExpanded) {
  1062           return;
  1065         this._matchedSelectorResponse = matched;
  1066         CssHtmlTree.processTemplate(this.templateMatchedSelectors,
  1067           this.matchedSelectorsContainer, this);
  1068         this.matchedExpander.setAttribute("open", "");
  1069         this.tree.styleInspector.inspector.emit("computed-view-property-expanded");
  1070       }).then(null, console.error);
  1071     } else {
  1072       this.matchedSelectorsContainer.innerHTML = "";
  1073       this.matchedExpander.removeAttribute("open");
  1074       this.tree.styleInspector.inspector.emit("computed-view-property-collapsed");
  1075       return promise.resolve(undefined);
  1077   },
  1079   get matchedSelectors()
  1081     return this._matchedSelectorResponse;
  1082   },
  1084   /**
  1085    * Provide access to the matched SelectorViews that we are currently
  1086    * displaying.
  1087    */
  1088   get matchedSelectorViews()
  1090     if (!this._matchedSelectorViews) {
  1091       this._matchedSelectorViews = [];
  1092       this._matchedSelectorResponse.forEach(
  1093         function matchedSelectorViews_convert(aSelectorInfo) {
  1094           this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
  1095         }, this);
  1098     return this._matchedSelectorViews;
  1099   },
  1101   /**
  1102    * Update all the selector source links to reflect whether we're linking to
  1103    * original sources (e.g. Sass files).
  1104    */
  1105   updateSourceLinks: function PropertyView_updateSourceLinks()
  1107     if (!this._matchedSelectorViews) {
  1108       return;
  1110     for (let view of this._matchedSelectorViews) {
  1111       view.updateSourceLink();
  1113   },
  1115   /**
  1116    * The action when a user expands matched selectors.
  1118    * @param {Event} aEvent Used to determine the class name of the targets click
  1119    * event.
  1120    */
  1121   onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
  1123     this.matchedExpanded = !this.matchedExpanded;
  1124     this.refreshMatchedSelectors();
  1125     aEvent.preventDefault();
  1126   },
  1128   /**
  1129    * The action when a user clicks on the MDN help link for a property.
  1130    */
  1131   mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
  1133     let inspector = this.tree.styleInspector.inspector;
  1135     if (inspector.target.tab) {
  1136       let browserWin = inspector.target.tab.ownerDocument.defaultView;
  1137       browserWin.openUILinkIn(this.link, "tab");
  1139     aEvent.preventDefault();
  1140   },
  1142   /**
  1143    * Destroy this property view, removing event listeners
  1144    */
  1145   destroy: function PropertyView_destroy() {
  1146     this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
  1147     this.element.removeEventListener("keydown", this.onKeyDown, false);
  1148     this.element = null;
  1150     this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false);
  1151     this.matchedExpander = null;
  1153     this.nameNode.removeEventListener("click", this.onFocus, false);
  1154     this.nameNode = null;
  1156     this.valueNode.removeEventListener("click", this.onFocus, false);
  1157     this.valueNode = null;
  1159 };
  1161 /**
  1162  * A container to give us easy access to display data from a CssRule
  1163  * @param CssHtmlTree aTree, the owning CssHtmlTree
  1164  * @param aSelectorInfo
  1165  */
  1166 function SelectorView(aTree, aSelectorInfo)
  1168   this.tree = aTree;
  1169   this.selectorInfo = aSelectorInfo;
  1170   this._cacheStatusNames();
  1172   this.updateSourceLink();
  1175 /**
  1176  * Decode for cssInfo.rule.status
  1177  * @see SelectorView.prototype._cacheStatusNames
  1178  * @see CssLogic.STATUS
  1179  */
  1180 SelectorView.STATUS_NAMES = [
  1181   // "Parent Match", "Matched", "Best Match"
  1182 ];
  1184 SelectorView.CLASS_NAMES = [
  1185   "parentmatch", "matched", "bestmatch"
  1186 ];
  1188 SelectorView.prototype = {
  1189   /**
  1190    * Cache localized status names.
  1192    * These statuses are localized inside the styleinspector.properties string
  1193    * bundle.
  1194    * @see css-logic.js - the CssLogic.STATUS array.
  1196    * @return {void}
  1197    */
  1198   _cacheStatusNames: function SelectorView_cacheStatusNames()
  1200     if (SelectorView.STATUS_NAMES.length) {
  1201       return;
  1204     for (let status in CssLogic.STATUS) {
  1205       let i = CssLogic.STATUS[status];
  1206       if (i > CssLogic.STATUS.UNMATCHED) {
  1207         let value = CssHtmlTree.l10n("rule.status." + status);
  1208         // Replace normal spaces with non-breaking spaces
  1209         SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0');
  1212   },
  1214   /**
  1215    * A localized version of cssRule.status
  1216    */
  1217   get statusText()
  1219     return SelectorView.STATUS_NAMES[this.selectorInfo.status];
  1220   },
  1222   /**
  1223    * Get class name for selector depending on status
  1224    */
  1225   get statusClass()
  1227     return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
  1228   },
  1230   get href()
  1232     if (this._href) {
  1233       return this._href;
  1235     let sheet = this.selectorInfo.rule.parentStyleSheet;
  1236     this._href = sheet ? sheet.href : "#";
  1237     return this._href;
  1238   },
  1240   get sourceText()
  1242     return this.selectorInfo.sourceText;
  1243   },
  1246   get value()
  1248     return this.selectorInfo.value;
  1249   },
  1251   get outputFragment()
  1253     // Sadly, because this fragment is added to the template by DOM Templater
  1254     // we lose any events that are attached. This means that URLs will open in a
  1255     // new window. At some point we should fix this by stopping using the
  1256     // templater.
  1257     let outputParser = this.tree._outputParser;
  1258     let frag = outputParser.parseCssProperty(
  1259       this.selectorInfo.name,
  1260       this.selectorInfo.value, {
  1261       colorSwatchClass: "computedview-colorswatch",
  1262       urlClass: "theme-link",
  1263       baseURI: this.selectorInfo.rule.href
  1264     });
  1265     return frag;
  1266   },
  1268   /**
  1269    * Update the text of the source link to reflect whether we're showing
  1270    * original sources or not.
  1271    */
  1272   updateSourceLink: function()
  1274     this.updateSource().then((oldSource) => {
  1275       if (oldSource != this.source && this.tree.propertyContainer) {
  1276         let selector = '[sourcelocation="' + oldSource + '"]';
  1277         let link = this.tree.propertyContainer.querySelector(selector);
  1278         if (link) {
  1279           link.textContent = this.source;
  1280           link.setAttribute("sourcelocation", this.source);
  1283     });
  1284   },
  1286   /**
  1287    * Update the 'source' store based on our original sources preference.
  1288    */
  1289   updateSource: function()
  1291     let rule = this.selectorInfo.rule;
  1292     this.sheet = rule.parentStyleSheet;
  1294     if (!rule || !this.sheet) {
  1295       let oldSource = this.source;
  1296       this.source = CssLogic.l10n("rule.sourceElement");
  1297       this.href = "#";
  1298       return promise.resolve(oldSource);
  1301     let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
  1303     if (showOrig && rule.type != ELEMENT_STYLE) {
  1304       let deferred = promise.defer();
  1306       // set as this first so we show something while we're fetching
  1307       this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
  1309       rule.getOriginalLocation().then(({href, line, column}) => {
  1310         let oldSource = this.source;
  1311         this.source = CssLogic.shortSource({href: href}) + ":" + line;
  1312         deferred.resolve(oldSource);
  1313       });
  1315       return deferred.promise;
  1318     let oldSource = this.source;
  1319     this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
  1320     return promise.resolve(oldSource);
  1321   },
  1323   /**
  1324    * Open the style editor if the RETURN key was pressed.
  1325    */
  1326   maybeOpenStyleEditor: function(aEvent)
  1328     let keyEvent = Ci.nsIDOMKeyEvent;
  1329     if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
  1330       this.openStyleEditor();
  1332   },
  1334   /**
  1335    * When a css link is clicked this method is called in order to either:
  1336    *   1. Open the link in view source (for chrome stylesheets).
  1337    *   2. Open the link in the style editor.
  1339    *   We can only view stylesheets contained in document.styleSheets inside the
  1340    *   style editor.
  1342    * @param aEvent The click event
  1343    */
  1344   openStyleEditor: function(aEvent)
  1346     let inspector = this.tree.styleInspector.inspector;
  1347     let rule = this.selectorInfo.rule;
  1349     // The style editor can only display stylesheets coming from content because
  1350     // chrome stylesheets are not listed in the editor's stylesheet selector.
  1351     //
  1352     // If the stylesheet is a content stylesheet we send it to the style
  1353     // editor else we display it in the view source window.
  1354     let sheet = rule.parentStyleSheet;
  1355     if (!sheet || sheet.isSystem) {
  1356       let contentDoc = null;
  1357       if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
  1358         let rawNode = this.tree.viewedElement.rawNode();
  1359         if (rawNode) {
  1360           contentDoc = rawNode.ownerDocument;
  1363       let viewSourceUtils = inspector.viewSourceUtils;
  1364       viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line);
  1365       return;
  1368     let location = promise.resolve({
  1369       href: rule.href,
  1370       line: rule.line
  1371     });
  1372     if (rule.href && Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
  1373       location = rule.getOriginalLocation();
  1376     location.then(({href, line}) => {
  1377       let target = inspector.target;
  1378       if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
  1379         gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
  1380           toolbox.getCurrentPanel().selectStyleSheet(href, line);
  1381         });
  1383     });
  1385 };
  1387 exports.CssHtmlTree = CssHtmlTree;
  1388 exports.PropertyView = PropertyView;

mercurial