Bumping gaia.json for 2 gaia revision(s) a=gaia-bump
[gecko.git] / browser / devtools / styleinspector / computed-view.js
blob5e1cd3ffbec80eff34dd696c8f36cd0c23a4e3be
1 /* -*- indent-tabs-mode: nil; js-indent-level: 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 {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
16 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
17 const overlays = require("devtools/styleinspector/style-inspector-overlays");
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)
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;
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   }
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).
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.
129  * @constructor
130  */
131 function CssHtmlTree(aStyleInspector, aPageStyle)
133   this.styleWindow = aStyleInspector.doc.defaultView;
134   this.styleDocument = aStyleInspector.doc;
135   this.styleInspector = aStyleInspector;
136   this.inspector = this.styleInspector.inspector;
137   this.pageStyle = aPageStyle;
138   this.propertyViews = [];
140   this._outputParser = new OutputParser();
142   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
143     getService(Ci.nsIXULChromeRegistry);
144   this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
146   // Create bound methods.
147   this.focusWindow = this.focusWindow.bind(this);
148   this._onContextMenu = this._onContextMenu.bind(this);
149   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
150   this._onSelectAll = this._onSelectAll.bind(this);
151   this._onClick = this._onClick.bind(this);
152   this._onCopy = this._onCopy.bind(this);
153   this._onCopyColor = this._onCopyColor.bind(this);
155   this.styleDocument.addEventListener("copy", this._onCopy);
156   this.styleDocument.addEventListener("mousedown", this.focusWindow);
157   this.styleDocument.addEventListener("contextmenu", this._onContextMenu);
159   // Nodes used in templating
160   this.root = this.styleDocument.getElementById("root");
161   this.templateRoot = this.styleDocument.getElementById("templateRoot");
162   this.element = this.styleDocument.getElementById("propertyContainer");
164   // Listen for click events
165   this.element.addEventListener("click", this._onClick, false);
167   // No results text.
168   this.noResults = this.styleDocument.getElementById("noResults");
170   // Refresh panel when color unit changed.
171   this._handlePrefChange = this._handlePrefChange.bind(this);
172   gDevTools.on("pref-changed", this._handlePrefChange);
174   // Refresh panel when pref for showing original sources changes
175   this._updateSourceLinks = this._updateSourceLinks.bind(this);
176   this._prefObserver = new PrefObserver("devtools.");
177   this._prefObserver.on(PREF_ORIG_SOURCES, this._updateSourceLinks);
179   CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
181   // The element that we're inspecting, and the document that it comes from.
182   this.viewedElement = null;
184   this._buildContextMenu();
185   this.createStyleViews();
187   // Add the tooltips and highlightersoverlay
188   this.tooltips = new overlays.TooltipsOverlay(this);
189   this.tooltips.addToView();
190   this.highlighters = new overlays.HighlightersOverlay(this);
191   this.highlighters.addToView();
195  * Memoized lookup of a l10n string from a string bundle.
196  * @param {string} aName The key to lookup.
197  * @returns A localized version of the given key.
198  */
199 CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
201   try {
202     return CssHtmlTree._strings.GetStringFromName(aName);
203   } catch (ex) {
204     Services.console.logStringMessage("Error reading '" + aName + "'");
205     throw new Error("l10n error with " + aName);
206   }
210  * Clone the given template node, and process it by resolving ${} references
211  * in the template.
213  * @param {nsIDOMElement} aTemplate the template note to use.
214  * @param {nsIDOMElement} aDestination the destination node where the
215  * processed nodes will be displayed.
216  * @param {object} aData the data to pass to the template.
217  * @param {Boolean} aPreserveDestination If true then the template will be
218  * appended to aDestination's content else aDestination.innerHTML will be
219  * cleared before the template is appended.
220  */
221 CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
222                                   aDestination, aData, aPreserveDestination)
224   if (!aPreserveDestination) {
225     aDestination.innerHTML = "";
226   }
228   // All the templater does is to populate a given DOM tree with the given
229   // values, so we need to clone the template first.
230   let duplicated = aTemplate.cloneNode(true);
232   // See https://github.com/mozilla/domtemplate/blob/master/README.md
233   // for docs on the template() function
234   template(duplicated, aData, { allowEval: true });
235   while (duplicated.firstChild) {
236     aDestination.appendChild(duplicated.firstChild);
237   }
240 XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
241         .createBundle("chrome://global/locale/devtools/styleinspector.properties"));
243 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
244   return Cc["@mozilla.org/widget/clipboardhelper;1"].
245     getService(Ci.nsIClipboardHelper);
248 CssHtmlTree.prototype = {
249   // Cache the list of properties that match the selected element.
250   _matchedProperties: null,
252   // Used for cancelling timeouts in the style filter.
253   _filterChangedTimeout: null,
255   // The search filter
256   searchField: null,
258   // Reference to the "Include browser styles" checkbox.
259   includeBrowserStylesCheckbox: null,
261   // Holds the ID of the panelRefresh timeout.
262   _panelRefreshTimeout: null,
264   // Toggle for zebra striping
265   _darkStripe: true,
267   // Number of visible properties
268   numVisibleProperties: 0,
270   setPageStyle: function(pageStyle) {
271     this.pageStyle = pageStyle;
272   },
274   get includeBrowserStyles()
275   {
276     return this.includeBrowserStylesCheckbox.checked;
277   },
279   _handlePrefChange: function(event, data) {
280     if (this._computed && (data.pref == "devtools.defaultColorUnit" ||
281         data.pref == PREF_ORIG_SOURCES)) {
282       this.refreshPanel();
283     }
284   },
286   /**
287    * Update the view with a new selected element.
288    * The CssHtmlTree panel will show the style information for the given element.
289    * @param {NodeFront} aElement The highlighted node to get styles for.
290    * @returns a promise that will be resolved when highlighting is complete.
291    */
292   selectElement: function(aElement) {
293     if (!aElement) {
294       this.viewedElement = null;
295       this.noResults.hidden = false;
297       if (this._refreshProcess) {
298         this._refreshProcess.cancel();
299       }
300       // Hiding all properties
301       for (let propView of this.propertyViews) {
302         propView.refresh();
303       }
304       return promise.resolve(undefined);
305     }
307     if (aElement === this.viewedElement) {
308       return promise.resolve(undefined);
309     }
311     this.viewedElement = aElement;
312     this.refreshSourceFilter();
314     return this.refreshPanel();
315   },
317   /**
318    * Get the type of a given node in the computed-view
319    * @param {DOMNode} node The node which we want information about
320    * @return {Object} The type information object contains the following props:
321    * - type {String} One of the VIEW_NODE_XXX_TYPE const in
322    *   style-inspector-overlays
323    * - value {Object} Depends on the type of the node
324    * returns null of the node isn't anything we care about
325    */
326   getNodeInfo: function(node) {
327     if (!node) {
328       return null;
329     }
331     let classes = node.classList;
333     // Check if the node isn't a selector first since this doesn't require
334     // walking the DOM
335     if (classes.contains("matched") ||
336         classes.contains("bestmatch") ||
337         classes.contains("parentmatch")) {
338       let selectorText = "";
339       for (let child of node.childNodes) {
340         if (child.nodeType === node.TEXT_NODE) {
341           selectorText += child.textContent;
342         }
343       }
344       return {
345         type: overlays.VIEW_NODE_SELECTOR_TYPE,
346         value: selectorText.trim()
347       }
348     }
350     // Walk up the nodes to find out where node is
351     let propertyView;
352     let propertyContent;
353     let parent = node;
354     while (parent.parentNode) {
355       if (parent.classList.contains("property-view")) {
356         propertyView = parent;
357         break;
358       }
359       if (parent.classList.contains("property-content")) {
360         propertyContent = parent;
361         break;
362       }
363       parent = parent.parentNode;
364     }
365     if (!propertyView && !propertyContent) {
366       return null;
367     }
369     let value, type;
371     // Get the property and value for a node that's a property name or value
372     let isHref = classes.contains("theme-link") && !classes.contains("link");
373     if (propertyView && (classes.contains("property-name") ||
374                          classes.contains("property-value") ||
375                          isHref)) {
376       value = {
377         property: parent.querySelector(".property-name").textContent,
378         value: parent.querySelector(".property-value").textContent
379       };
380     }
381     if (propertyContent && (classes.contains("other-property-value") ||
382                             isHref)) {
383       let view = propertyContent.previousSibling;
384       value = {
385         property: view.querySelector(".property-name").textContent,
386         value: node.textContent
387       };
388     }
390     // Get the type
391     if (classes.contains("property-name")) {
392       type = overlays.VIEW_NODE_PROPERTY_TYPE;
393     } else if (classes.contains("property-value") ||
394                classes.contains("other-property-value")) {
395       type = overlays.VIEW_NODE_VALUE_TYPE;
396     } else if (isHref) {
397       type = overlays.VIEW_NODE_IMAGE_URL_TYPE;
398       value.url = node.href;
399     } else {
400       return null;
401     }
403     return {type, value};
404   },
406   _createPropertyViews: function()
407   {
408     if (this._createViewsPromise) {
409       return this._createViewsPromise;
410     }
412     let deferred = promise.defer();
413     this._createViewsPromise = deferred.promise;
415     this.refreshSourceFilter();
416     this.numVisibleProperties = 0;
417     let fragment = this.styleDocument.createDocumentFragment();
419     this._createViewsProcess = new UpdateProcess(this.styleWindow, CssHtmlTree.propertyNames, {
420       onItem: (aPropertyName) => {
421         // Per-item callback.
422         let propView = new PropertyView(this, aPropertyName);
423         fragment.appendChild(propView.buildMain());
424         fragment.appendChild(propView.buildSelectorContainer());
426         if (propView.visible) {
427           this.numVisibleProperties++;
428         }
429         this.propertyViews.push(propView);
430       },
431       onCancel: () => {
432         deferred.reject("_createPropertyViews cancelled");
433       },
434       onDone: () => {
435         // Completed callback.
436         this.element.appendChild(fragment);
437         this.noResults.hidden = this.numVisibleProperties > 0;
438         deferred.resolve(undefined);
439       }
440     });
442     this._createViewsProcess.schedule();
443     return deferred.promise;
444   },
446   /**
447    * Refresh the panel content.
448    */
449   refreshPanel: function CssHtmlTree_refreshPanel()
450   {
451     if (!this.viewedElement) {
452       return promise.resolve();
453     }
455     // Capture the current viewed element to return from the promise handler
456     // early if it changed
457     let viewedElement = this.viewedElement;
459     return promise.all([
460       this._createPropertyViews(),
461       this.pageStyle.getComputed(this.viewedElement, {
462         filter: this._sourceFilter,
463         onlyMatched: !this.includeBrowserStyles,
464         markMatched: true
465       })
466     ]).then(([createViews, computed]) => {
467       if (viewedElement !== this.viewedElement) {
468         return;
469       }
471       this._matchedProperties = new Set;
472       for (let name in computed) {
473         if (computed[name].matched) {
474           this._matchedProperties.add(name);
475         }
476       }
477       this._computed = computed;
479       if (this._refreshProcess) {
480         this._refreshProcess.cancel();
481       }
483       this.noResults.hidden = true;
485       // Reset visible property count
486       this.numVisibleProperties = 0;
488       // Reset zebra striping.
489       this._darkStripe = true;
491       let deferred = promise.defer();
492       this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
493         onItem: (aPropView) => {
494           aPropView.refresh();
495         },
496         onDone: () => {
497           this._refreshProcess = null;
498           this.noResults.hidden = this.numVisibleProperties > 0;
499           this.inspector.emit("computed-view-refreshed");
500           deferred.resolve(undefined);
501         }
502       });
503       this._refreshProcess.schedule();
504       return deferred.promise;
505     }).then(null, (err) => console.error(err));
506   },
508   /**
509    * Called when the user enters a search term.
510    *
511    * @param {Event} aEvent the DOM Event object.
512    */
513   filterChanged: function CssHtmlTree_filterChanged(aEvent)
514   {
515     let win = this.styleWindow;
517     if (this._filterChangedTimeout) {
518       win.clearTimeout(this._filterChangedTimeout);
519     }
521     this._filterChangedTimeout = win.setTimeout(() => {
522       this.refreshPanel();
523       this._filterChangeTimeout = null;
524     }, FILTER_CHANGED_TIMEOUT);
525   },
527   /**
528    * The change event handler for the includeBrowserStyles checkbox.
529    *
530    * @param {Event} aEvent the DOM Event object.
531    */
532   includeBrowserStylesChanged:
533   function CssHtmltree_includeBrowserStylesChanged(aEvent)
534   {
535     this.refreshSourceFilter();
536     this.refreshPanel();
537   },
539   /**
540    * When includeBrowserStyles.checked is false we only display properties that
541    * have matched selectors and have been included by the document or one of the
542    * document's stylesheets. If .checked is false we display all properties
543    * including those that come from UA stylesheets.
544    */
545   refreshSourceFilter: function CssHtmlTree_setSourceFilter()
546   {
547     this._matchedProperties = null;
548     this._sourceFilter = this.includeBrowserStyles ?
549                                  CssLogic.FILTER.UA :
550                                  CssLogic.FILTER.USER;
551   },
553   _updateSourceLinks: function CssHtmlTree__updateSourceLinks()
554   {
555     for (let propView of this.propertyViews) {
556       propView.updateSourceLinks();
557     }
558     this.inspector.emit("computed-view-sourcelinks-updated");
559   },
561   /**
562    * The CSS as displayed by the UI.
563    */
564   createStyleViews: function CssHtmlTree_createStyleViews()
565   {
566     if (CssHtmlTree.propertyNames) {
567       return;
568     }
570     CssHtmlTree.propertyNames = [];
572     // Here we build and cache a list of css properties supported by the browser
573     // We could use any element but let's use the main document's root element
574     let styles = this.styleWindow.getComputedStyle(this.styleDocument.documentElement);
575     let mozProps = [];
576     for (let i = 0, numStyles = styles.length; i < numStyles; i++) {
577       let prop = styles.item(i);
578       if (prop.startsWith("--")) {
579         // Skip any CSS variables used inside of browser CSS files
580         continue;
581       } else if (prop.startsWith("-")) {
582         mozProps.push(prop);
583       } else {
584         CssHtmlTree.propertyNames.push(prop);
585       }
586     }
588     CssHtmlTree.propertyNames.sort();
589     CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames,
590       mozProps.sort());
592     this._createPropertyViews().then(null, e => {
593       if (!this.styleInspector) {
594         console.warn("The creation of property views was cancelled because the " +
595           "computed-view was destroyed before it was done creating views");
596       } else {
597         console.error(e);
598       }
599     });
600   },
602   /**
603    * Get a set of properties that have matched selectors.
604    *
605    * @return {Set} If a property name is in the set, it has matching selectors.
606    */
607   get matchedProperties()
608   {
609     return this._matchedProperties || new Set;
610   },
612   /**
613    * Focus the window on mousedown.
614    *
615    * @param aEvent The event object
616    */
617   focusWindow: function(aEvent)
618   {
619     let win = this.styleDocument.defaultView;
620     win.focus();
621   },
623   /**
624    * Create a context menu.
625    */
626   _buildContextMenu: function()
627   {
628     let doc = this.styleDocument.defaultView.parent.document;
630     this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup");
631     this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
632     this._contextmenu.id = "computed-view-context-menu";
634     // Select All
635     this.menuitemSelectAll = createMenuItem(this._contextmenu, {
636       label: "computedView.contextmenu.selectAll",
637       accesskey: "computedView.contextmenu.selectAll.accessKey",
638       command: this._onSelectAll
639     });
641     // Copy
642     this.menuitemCopy = createMenuItem(this._contextmenu, {
643       label: "computedView.contextmenu.copy",
644       accesskey: "computedView.contextmenu.copy.accessKey",
645       command: this._onCopy
646     });
648     // Copy color
649     this.menuitemCopyColor = createMenuItem(this._contextmenu, {
650       label: "ruleView.contextmenu.copyColor",
651       accesskey: "ruleView.contextmenu.copyColor.accessKey",
652       command: this._onCopyColor
653     });
655     // Show Original Sources
656     this.menuitemSources= createMenuItem(this._contextmenu, {
657       label: "ruleView.contextmenu.showOrigSources",
658       accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
659       command: this._onToggleOrigSources,
660       type: "checkbox"
661     });
663     let popupset = doc.documentElement.querySelector("popupset");
664     if (!popupset) {
665       popupset = doc.createElementNS(XUL_NS, "popupset");
666       doc.documentElement.appendChild(popupset);
667     }
668     popupset.appendChild(this._contextmenu);
669   },
671   /**
672    * Update the context menu. This means enabling or disabling menuitems as
673    * appropriate.
674    */
675   _contextMenuUpdate: function()
676   {
677     let win = this.styleDocument.defaultView;
678     let disable = win.getSelection().isCollapsed;
679     this.menuitemCopy.disabled = disable;
681     let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
682     this.menuitemSources.setAttribute("checked", showOrig);
684     this.menuitemCopyColor.hidden = !this._isColorPopup();
685   },
687   /**
688    * A helper that determines if the popup was opened with a click to a color
689    * value and saves the color to this._colorToCopy.
690    *
691    * @return {Boolean}
692    *         true if click on color opened the popup, false otherwise.
693    */
694   _isColorPopup: function () {
695     this._colorToCopy = "";
697     let trigger = this.popupNode;
698     if (!trigger) {
699       return false;
700     }
702     let container = (trigger.nodeType == trigger.TEXT_NODE) ?
703                      trigger.parentElement : trigger;
705     let isColorNode = el => el.dataset && "color" in el.dataset;
707     while (!isColorNode(container)) {
708       container = container.parentNode;
709       if (!container) {
710         return false;
711       }
712     }
714     this._colorToCopy = container.dataset["color"];
715     return true;
716   },
718   /**
719    * Context menu handler.
720    */
721   _onContextMenu: function(event) {
722     try {
723       this.popupNode = event.explicitOriginalTarget;
724       this.styleDocument.defaultView.focus();
725       this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
726     } catch(e) {
727       console.error(e);
728     }
729   },
731   /**
732    * Select all text.
733    */
734   _onSelectAll: function()
735   {
736     try {
737       let win = this.styleDocument.defaultView;
738       let selection = win.getSelection();
740       selection.selectAllChildren(this.styleDocument.documentElement);
741     } catch(e) {
742       console.error(e);
743     }
744   },
746   _onClick: function(event) {
747     let target = event.target;
749     if (target.nodeName === "a") {
750       event.stopPropagation();
751       event.preventDefault();
752       let browserWin = this.inspector.target.tab.ownerDocument.defaultView;
753       browserWin.openUILinkIn(target.href, "tab");
754     }
755   },
757   _onCopyColor: function() {
758     clipboardHelper.copyString(this._colorToCopy, this.styleDocument);
759   },
761   /**
762    * Copy selected text.
763    *
764    * @param event The event object
765    */
766   _onCopy: function(event)
767   {
768     try {
769       let win = this.styleDocument.defaultView;
770       let text = win.getSelection().toString().trim();
772       // Tidy up block headings by moving CSS property names and their values onto
773       // the same line and inserting a colon between them.
774       let textArray = text.split(/[\r\n]+/);
775       let result = "";
777       // Parse text array to output string.
778       if (textArray.length > 1) {
779         for (let prop of textArray) {
780           if (CssHtmlTree.propertyNames.indexOf(prop) !== -1) {
781             // Property name
782             result += prop;
783           } else {
784             // Property value
785             result += ": " + prop;
786             if (result.length > 0) {
787               result += ";\n";
788             }
789           }
790         }
791       } else {
792         // Short text fragment.
793         result = textArray[0];
794       }
796       clipboardHelper.copyString(result, this.styleDocument);
798       if (event) {
799         event.preventDefault();
800       }
801     } catch(e) {
802       console.error(e);
803     }
804   },
806   /**
807    *  Toggle the original sources pref.
808    */
809   _onToggleOrigSources: function()
810   {
811     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
812     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
813   },
815   /**
816    * Destructor for CssHtmlTree.
817    */
818   destroy: function CssHtmlTree_destroy()
819   {
820     this.viewedElement = null;
821     this._outputParser = null;
823     // Remove event listeners
824     this.includeBrowserStylesCheckbox.removeEventListener("command",
825       this.includeBrowserStylesChanged);
826     this.searchField.removeEventListener("command", this.filterChanged);
827     gDevTools.off("pref-changed", this._handlePrefChange);
829     this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks);
830     this._prefObserver.destroy();
832     // Cancel tree construction
833     if (this._createViewsProcess) {
834       this._createViewsProcess.cancel();
835     }
836     if (this._refreshProcess) {
837       this._refreshProcess.cancel();
838     }
840     this.element.removeEventListener("click", this._onClick, false);
842     // Remove context menu
843     if (this._contextmenu) {
844       // Destroy the Select All menuitem.
845       this.menuitemCopy.removeEventListener("command", this._onCopy);
846       this.menuitemCopy = null;
848       // Destroy the Copy menuitem.
849       this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
850       this.menuitemSelectAll = null;
852       // Destroy Copy Color menuitem.
853       this.menuitemCopyColor.removeEventListener("command", this._onCopyColor);
854       this.menuitemCopyColor = null;
856       // Destroy the context menu.
857       this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
858       this._contextmenu.parentNode.removeChild(this._contextmenu);
859       this._contextmenu = null;
860     }
862     this.popupNode = null;
864     this.tooltips.destroy();
865     this.highlighters.destroy();
867     // Remove bound listeners
868     this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
869     this.styleDocument.removeEventListener("copy", this._onCopy);
870     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
872     // Nodes used in templating
873     this.root = null;
874     this.element = null;
875     this.panel = null;
877     // The document in which we display the results (csshtmltree.xul).
878     this.styleDocument = null;
880     for (let propView of this.propertyViews)  {
881       propView.destroy();
882     }
884     // The element that we're inspecting, and the document that it comes from.
885     this.propertyViews = null;
886     this.styleWindow = null;
887     this.styleDocument = null;
888     this.styleInspector = null;
889   }
892 function PropertyInfo(aTree, aName) {
893   this.tree = aTree;
894   this.name = aName;
896 PropertyInfo.prototype = {
897   get value() {
898     if (this.tree._computed) {
899       let value = this.tree._computed[this.name].value;
900       return value;
901     }
902   }
905 function createMenuItem(aMenu, aAttributes)
907   let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
909   item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label));
910   item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey));
911   item.addEventListener("command", aAttributes.command);
913   aMenu.appendChild(item);
915   return item;
919  * A container to give easy access to property data from the template engine.
921  * @constructor
922  * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with.
923  * @param {string} aName the CSS property name for which this PropertyView
924  * instance will render the rules.
925  */
926 function PropertyView(aTree, aName)
928   this.tree = aTree;
929   this.name = aName;
930   this.getRTLAttr = aTree.getRTLAttr;
932   this.link = "https://developer.mozilla.org/CSS/" + aName;
934   this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors");
935   this._propertyInfo = new PropertyInfo(aTree, aName);
938 PropertyView.prototype = {
939   // The parent element which contains the open attribute
940   element: null,
942   // Property header node
943   propertyHeader: null,
945   // Destination for property names
946   nameNode: null,
948   // Destination for property values
949   valueNode: null,
951   // Are matched rules expanded?
952   matchedExpanded: false,
954   // Matched selector container
955   matchedSelectorsContainer: null,
957   // Matched selector expando
958   matchedExpander: null,
960   // Cache for matched selector views
961   _matchedSelectorViews: null,
963   // The previously selected element used for the selector view caches
964   prevViewedElement: null,
966   /**
967    * Get the computed style for the current property.
968    *
969    * @return {string} the computed style for the current property of the
970    * currently highlighted element.
971    */
972   get value()
973   {
974     return this.propertyInfo.value;
975   },
977   /**
978    * An easy way to access the CssPropertyInfo behind this PropertyView.
979    */
980   get propertyInfo()
981   {
982     return this._propertyInfo;
983   },
985   /**
986    * Does the property have any matched selectors?
987    */
988   get hasMatchedSelectors()
989   {
990     return this.tree.matchedProperties.has(this.name);
991   },
993   /**
994    * Should this property be visible?
995    */
996   get visible()
997   {
998     if (!this.tree.viewedElement) {
999       return false;
1000     }
1002     if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
1003       return false;
1004     }
1006     let searchTerm = this.tree.searchField.value.toLowerCase();
1007     if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 &&
1008       this.value.toLowerCase().indexOf(searchTerm) == -1) {
1009       return false;
1010     }
1012     return true;
1013   },
1015   /**
1016    * Returns the className that should be assigned to the propertyView.
1017    * @return string
1018    */
1019   get propertyHeaderClassName()
1020   {
1021     if (this.visible) {
1022       let isDark = this.tree._darkStripe = !this.tree._darkStripe;
1023       return isDark ? "property-view row-striped" : "property-view";
1024     }
1025     return "property-view-hidden";
1026   },
1028   /**
1029    * Returns the className that should be assigned to the propertyView content
1030    * container.
1031    * @return string
1032    */
1033   get propertyContentClassName()
1034   {
1035     if (this.visible) {
1036       let isDark = this.tree._darkStripe;
1037       return isDark ? "property-content row-striped" : "property-content";
1038     }
1039     return "property-content-hidden";
1040   },
1042   /**
1043    * Build the markup for on computed style
1044    * @return Element
1045    */
1046   buildMain: function PropertyView_buildMain()
1047   {
1048     let doc = this.tree.styleDocument;
1050     // Build the container element
1051     this.onMatchedToggle = this.onMatchedToggle.bind(this);
1052     this.element = doc.createElementNS(HTML_NS, "div");
1053     this.element.setAttribute("class", this.propertyHeaderClassName);
1054     this.element.addEventListener("dblclick", this.onMatchedToggle, false);
1056     // Make it keyboard navigable
1057     this.element.setAttribute("tabindex", "0");
1058     this.onKeyDown = (aEvent) => {
1059       let keyEvent = Ci.nsIDOMKeyEvent;
1060       if (aEvent.keyCode == keyEvent.DOM_VK_F1) {
1061         this.mdnLinkClick();
1062       }
1063       if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
1064         aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
1065         this.onMatchedToggle(aEvent);
1066       }
1067     };
1068     this.element.addEventListener("keydown", this.onKeyDown, false);
1070     // Build the twisty expand/collapse
1071     this.matchedExpander = doc.createElementNS(HTML_NS, "div");
1072     this.matchedExpander.className = "expander theme-twisty";
1073     this.matchedExpander.addEventListener("click", this.onMatchedToggle, false);
1074     this.element.appendChild(this.matchedExpander);
1076     this.focusElement = () => this.element.focus();
1078     // Build the style name element
1079     this.nameNode = doc.createElementNS(HTML_NS, "div");
1080     this.nameNode.setAttribute("class", "property-name theme-fg-color5");
1081     // Reset its tabindex attribute otherwise, if an ellipsis is applied
1082     // it will be reachable via TABing
1083     this.nameNode.setAttribute("tabindex", "");
1084     this.nameNode.textContent = this.nameNode.title = this.name;
1085     // Make it hand over the focus to the container
1086     this.onFocus = () => this.element.focus();
1087     this.nameNode.addEventListener("click", this.onFocus, false);
1088     this.element.appendChild(this.nameNode);
1090     // Build the style value element
1091     this.valueNode = doc.createElementNS(HTML_NS, "div");
1092     this.valueNode.setAttribute("class", "property-value theme-fg-color1");
1093     // Reset its tabindex attribute otherwise, if an ellipsis is applied
1094     // it will be reachable via TABing
1095     this.valueNode.setAttribute("tabindex", "");
1096     this.valueNode.setAttribute("dir", "ltr");
1097     // Make it hand over the focus to the container
1098     this.valueNode.addEventListener("click", this.onFocus, false);
1099     this.element.appendChild(this.valueNode);
1101     return this.element;
1102   },
1104   buildSelectorContainer: function PropertyView_buildSelectorContainer()
1105   {
1106     let doc = this.tree.styleDocument;
1107     let element = doc.createElementNS(HTML_NS, "div");
1108     element.setAttribute("class", this.propertyContentClassName);
1109     this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div");
1110     this.matchedSelectorsContainer.setAttribute("class", "matchedselectors");
1111     element.appendChild(this.matchedSelectorsContainer);
1113     return element;
1114   },
1116   /**
1117    * Refresh the panel's CSS property value.
1118    */
1119   refresh: function PropertyView_refresh()
1120   {
1121     this.element.className = this.propertyHeaderClassName;
1122     this.element.nextElementSibling.className = this.propertyContentClassName;
1124     if (this.prevViewedElement != this.tree.viewedElement) {
1125       this._matchedSelectorViews = null;
1126       this.prevViewedElement = this.tree.viewedElement;
1127     }
1129     if (!this.tree.viewedElement || !this.visible) {
1130       this.valueNode.textContent = this.valueNode.title = "";
1131       this.matchedSelectorsContainer.parentNode.hidden = true;
1132       this.matchedSelectorsContainer.textContent = "";
1133       this.matchedExpander.removeAttribute("open");
1134       return;
1135     }
1137     this.tree.numVisibleProperties++;
1139     let outputParser = this.tree._outputParser;
1140     let frag = outputParser.parseCssProperty(this.propertyInfo.name,
1141       this.propertyInfo.value,
1142       {
1143         colorSwatchClass: "computedview-colorswatch",
1144         urlClass: "theme-link"
1145         // No need to use baseURI here as computed URIs are never relative.
1146       });
1147     this.valueNode.innerHTML = "";
1148     this.valueNode.appendChild(frag);
1150     this.refreshMatchedSelectors();
1151   },
1153   /**
1154    * Refresh the panel matched rules.
1155    */
1156   refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
1157   {
1158     let hasMatchedSelectors = this.hasMatchedSelectors;
1159     this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
1161     if (hasMatchedSelectors) {
1162       this.matchedExpander.classList.add("expandable");
1163     } else {
1164       this.matchedExpander.classList.remove("expandable");
1165     }
1167     if (this.matchedExpanded && hasMatchedSelectors) {
1168       return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => {
1169         if (!this.matchedExpanded) {
1170           return;
1171         }
1173         this._matchedSelectorResponse = matched;
1174         CssHtmlTree.processTemplate(this.templateMatchedSelectors,
1175           this.matchedSelectorsContainer, this);
1176         this.matchedExpander.setAttribute("open", "");
1177         this.tree.inspector.emit("computed-view-property-expanded");
1178       }).then(null, console.error);
1179     } else {
1180       this.matchedSelectorsContainer.innerHTML = "";
1181       this.matchedExpander.removeAttribute("open");
1182       this.tree.inspector.emit("computed-view-property-collapsed");
1183       return promise.resolve(undefined);
1184     }
1185   },
1187   get matchedSelectors()
1188   {
1189     return this._matchedSelectorResponse;
1190   },
1192   /**
1193    * Provide access to the matched SelectorViews that we are currently
1194    * displaying.
1195    */
1196   get matchedSelectorViews()
1197   {
1198     if (!this._matchedSelectorViews) {
1199       this._matchedSelectorViews = [];
1200       this._matchedSelectorResponse.forEach(
1201         function matchedSelectorViews_convert(aSelectorInfo) {
1202           this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
1203         }, this);
1204     }
1206     return this._matchedSelectorViews;
1207   },
1209   /**
1210    * Update all the selector source links to reflect whether we're linking to
1211    * original sources (e.g. Sass files).
1212    */
1213   updateSourceLinks: function PropertyView_updateSourceLinks()
1214   {
1215     if (!this._matchedSelectorViews) {
1216       return;
1217     }
1218     for (let view of this._matchedSelectorViews) {
1219       view.updateSourceLink();
1220     }
1221   },
1223   /**
1224    * The action when a user expands matched selectors.
1225    *
1226    * @param {Event} aEvent Used to determine the class name of the targets click
1227    * event.
1228    */
1229   onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
1230   {
1231     this.matchedExpanded = !this.matchedExpanded;
1232     this.refreshMatchedSelectors();
1233     aEvent.preventDefault();
1234   },
1236   /**
1237    * The action when a user clicks on the MDN help link for a property.
1238    */
1239   mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
1240   {
1241     let inspector = this.tree.inspector;
1243     if (inspector.target.tab) {
1244       let browserWin = inspector.target.tab.ownerDocument.defaultView;
1245       browserWin.openUILinkIn(this.link, "tab");
1246     }
1247     aEvent.preventDefault();
1248   },
1250   /**
1251    * Destroy this property view, removing event listeners
1252    */
1253   destroy: function PropertyView_destroy() {
1254     this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
1255     this.element.removeEventListener("keydown", this.onKeyDown, false);
1256     this.element = null;
1258     this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false);
1259     this.matchedExpander = null;
1261     this.nameNode.removeEventListener("click", this.onFocus, false);
1262     this.nameNode = null;
1264     this.valueNode.removeEventListener("click", this.onFocus, false);
1265     this.valueNode = null;
1266   }
1270  * A container to give us easy access to display data from a CssRule
1271  * @param CssHtmlTree aTree, the owning CssHtmlTree
1272  * @param aSelectorInfo
1273  */
1274 function SelectorView(aTree, aSelectorInfo)
1276   this.tree = aTree;
1277   this.selectorInfo = aSelectorInfo;
1278   this._cacheStatusNames();
1280   this.updateSourceLink();
1284  * Decode for cssInfo.rule.status
1285  * @see SelectorView.prototype._cacheStatusNames
1286  * @see CssLogic.STATUS
1287  */
1288 SelectorView.STATUS_NAMES = [
1289   // "Parent Match", "Matched", "Best Match"
1292 SelectorView.CLASS_NAMES = [
1293   "parentmatch", "matched", "bestmatch"
1296 SelectorView.prototype = {
1297   /**
1298    * Cache localized status names.
1299    *
1300    * These statuses are localized inside the styleinspector.properties string
1301    * bundle.
1302    * @see css-logic.js - the CssLogic.STATUS array.
1303    *
1304    * @return {void}
1305    */
1306   _cacheStatusNames: function SelectorView_cacheStatusNames()
1307   {
1308     if (SelectorView.STATUS_NAMES.length) {
1309       return;
1310     }
1312     for (let status in CssLogic.STATUS) {
1313       let i = CssLogic.STATUS[status];
1314       if (i > CssLogic.STATUS.UNMATCHED) {
1315         let value = CssHtmlTree.l10n("rule.status." + status);
1316         // Replace normal spaces with non-breaking spaces
1317         SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0');
1318       }
1319     }
1320   },
1322   /**
1323    * A localized version of cssRule.status
1324    */
1325   get statusText()
1326   {
1327     return SelectorView.STATUS_NAMES[this.selectorInfo.status];
1328   },
1330   /**
1331    * Get class name for selector depending on status
1332    */
1333   get statusClass()
1334   {
1335     return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
1336   },
1338   get href()
1339   {
1340     if (this._href) {
1341       return this._href;
1342     }
1343     let sheet = this.selectorInfo.rule.parentStyleSheet;
1344     this._href = sheet ? sheet.href : "#";
1345     return this._href;
1346   },
1348   get sourceText()
1349   {
1350     return this.selectorInfo.sourceText;
1351   },
1354   get value()
1355   {
1356     return this.selectorInfo.value;
1357   },
1359   get outputFragment()
1360   {
1361     // Sadly, because this fragment is added to the template by DOM Templater
1362     // we lose any events that are attached. This means that URLs will open in a
1363     // new window. At some point we should fix this by stopping using the
1364     // templater.
1365     let outputParser = this.tree._outputParser;
1366     let frag = outputParser.parseCssProperty(
1367       this.selectorInfo.name,
1368       this.selectorInfo.value, {
1369       colorSwatchClass: "computedview-colorswatch",
1370       urlClass: "theme-link",
1371       baseURI: this.selectorInfo.rule.href
1372     });
1373     return frag;
1374   },
1376   /**
1377    * Update the text of the source link to reflect whether we're showing
1378    * original sources or not.
1379    */
1380   updateSourceLink: function()
1381   {
1382     return this.updateSource().then((oldSource) => {
1383       if (oldSource != this.source && this.tree.element) {
1384         let selector = '[sourcelocation="' + oldSource + '"]';
1385         let link = this.tree.element.querySelector(selector);
1386         if (link) {
1387           link.textContent = this.source;
1388           link.setAttribute("sourcelocation", this.source);
1389         }
1390       }
1391     });
1392   },
1394   /**
1395    * Update the 'source' store based on our original sources preference.
1396    */
1397   updateSource: function()
1398   {
1399     let rule = this.selectorInfo.rule;
1400     this.sheet = rule.parentStyleSheet;
1402     if (!rule || !this.sheet) {
1403       let oldSource = this.source;
1404       this.source = CssLogic.l10n("rule.sourceElement");
1405       this.href = "#";
1406       return promise.resolve(oldSource);
1407     }
1409     let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
1411     if (showOrig && rule.type != ELEMENT_STYLE) {
1412       let deferred = promise.defer();
1414       // set as this first so we show something while we're fetching
1415       this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
1417       rule.getOriginalLocation().then(({href, line, column}) => {
1418         let oldSource = this.source;
1419         this.source = CssLogic.shortSource({href: href}) + ":" + line;
1420         deferred.resolve(oldSource);
1421       });
1423       return deferred.promise;
1424     }
1426     let oldSource = this.source;
1427     this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
1428     return promise.resolve(oldSource);
1429   },
1431   /**
1432    * Open the style editor if the RETURN key was pressed.
1433    */
1434   maybeOpenStyleEditor: function(aEvent)
1435   {
1436     let keyEvent = Ci.nsIDOMKeyEvent;
1437     if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
1438       this.openStyleEditor();
1439     }
1440   },
1442   /**
1443    * When a css link is clicked this method is called in order to either:
1444    *   1. Open the link in view source (for chrome stylesheets).
1445    *   2. Open the link in the style editor.
1446    *
1447    *   We can only view stylesheets contained in document.styleSheets inside the
1448    *   style editor.
1449    *
1450    * @param aEvent The click event
1451    */
1452   openStyleEditor: function(aEvent)
1453   {
1454     let inspector = this.tree.inspector;
1455     let rule = this.selectorInfo.rule;
1457     // The style editor can only display stylesheets coming from content because
1458     // chrome stylesheets are not listed in the editor's stylesheet selector.
1459     //
1460     // If the stylesheet is a content stylesheet we send it to the style
1461     // editor else we display it in the view source window.
1462     let sheet = rule.parentStyleSheet;
1463     if (!sheet || sheet.isSystem) {
1464       let contentDoc = null;
1465       if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
1466         let rawNode = this.tree.viewedElement.rawNode();
1467         if (rawNode) {
1468           contentDoc = rawNode.ownerDocument;
1469         }
1470       }
1471       let viewSourceUtils = inspector.viewSourceUtils;
1472       viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line);
1473       return;
1474     }
1476     let location = promise.resolve(rule.location);
1477     if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
1478       location = rule.getOriginalLocation();
1479     }
1480     location.then(({source, href, line, column}) => {
1481       let target = inspector.target;
1482       if (ToolDefinitions.styleEditor.isTargetSupported(target)) {
1483         gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) {
1484           let sheet = source || href;
1485           toolbox.getCurrentPanel().selectStyleSheet(sheet, line, column);
1486         });
1487       }
1488     });
1489   }
1492 exports.CssHtmlTree = CssHtmlTree;
1493 exports.PropertyView = PropertyView;