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";
31 * Helper for long-running processes that should yield occasionally to
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.
49 function UpdateProcess(aWin, aGenerator, aOptions)
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 = {
64 * Schedule a new batch on the main loop.
66 schedule: function UP_schedule()
71 this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0);
75 * Cancel the running process. onItem will not be called again,
76 * and onCancel will be called.
78 cancel: function UP_cancel()
81 this.win.clearTimeout(this._timeout);
88 _timeoutHandler: function UP_timeoutHandler() {
94 if (e instanceof StopIteration) {
104 _runBatch: function Y_runBatch()
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) {
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.
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);
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.
199 CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
202 return CssHtmlTree._strings.GetStringFromName(aName);
204 Services.console.logStringMessage("Error reading '" + aName + "'");
205 throw new Error("l10n error with " + aName);
210 * Clone the given template node, and process it by resolving ${} references
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.
221 CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
222 aDestination, aData, aPreserveDestination)
224 if (!aPreserveDestination) {
225 aDestination.innerHTML = "";
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);
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,
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
267 // Number of visible properties
268 numVisibleProperties: 0,
270 setPageStyle: function(pageStyle) {
271 this.pageStyle = pageStyle;
274 get includeBrowserStyles()
276 return this.includeBrowserStylesCheckbox.checked;
279 _handlePrefChange: function(event, data) {
280 if (this._computed && (data.pref == "devtools.defaultColorUnit" ||
281 data.pref == PREF_ORIG_SOURCES)) {
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.
292 selectElement: function(aElement) {
294 this.viewedElement = null;
295 this.noResults.hidden = false;
297 if (this._refreshProcess) {
298 this._refreshProcess.cancel();
300 // Hiding all properties
301 for (let propView of this.propertyViews) {
304 return promise.resolve(undefined);
307 if (aElement === this.viewedElement) {
308 return promise.resolve(undefined);
311 this.viewedElement = aElement;
312 this.refreshSourceFilter();
314 return this.refreshPanel();
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
326 getNodeInfo: function(node) {
331 let classes = node.classList;
333 // Check if the node isn't a selector first since this doesn't require
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;
345 type: overlays.VIEW_NODE_SELECTOR_TYPE,
346 value: selectorText.trim()
350 // Walk up the nodes to find out where node is
354 while (parent.parentNode) {
355 if (parent.classList.contains("property-view")) {
356 propertyView = parent;
359 if (parent.classList.contains("property-content")) {
360 propertyContent = parent;
363 parent = parent.parentNode;
365 if (!propertyView && !propertyContent) {
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") ||
377 property: parent.querySelector(".property-name").textContent,
378 value: parent.querySelector(".property-value").textContent
381 if (propertyContent && (classes.contains("other-property-value") ||
383 let view = propertyContent.previousSibling;
385 property: view.querySelector(".property-name").textContent,
386 value: node.textContent
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;
397 type = overlays.VIEW_NODE_IMAGE_URL_TYPE;
398 value.url = node.href;
403 return {type, value};
406 _createPropertyViews: function()
408 if (this._createViewsPromise) {
409 return this._createViewsPromise;
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++;
429 this.propertyViews.push(propView);
432 deferred.reject("_createPropertyViews cancelled");
435 // Completed callback.
436 this.element.appendChild(fragment);
437 this.noResults.hidden = this.numVisibleProperties > 0;
438 deferred.resolve(undefined);
442 this._createViewsProcess.schedule();
443 return deferred.promise;
447 * Refresh the panel content.
449 refreshPanel: function CssHtmlTree_refreshPanel()
451 if (!this.viewedElement) {
452 return promise.resolve();
455 // Capture the current viewed element to return from the promise handler
456 // early if it changed
457 let viewedElement = this.viewedElement;
460 this._createPropertyViews(),
461 this.pageStyle.getComputed(this.viewedElement, {
462 filter: this._sourceFilter,
463 onlyMatched: !this.includeBrowserStyles,
466 ]).then(([createViews, computed]) => {
467 if (viewedElement !== this.viewedElement) {
471 this._matchedProperties = new Set;
472 for (let name in computed) {
473 if (computed[name].matched) {
474 this._matchedProperties.add(name);
477 this._computed = computed;
479 if (this._refreshProcess) {
480 this._refreshProcess.cancel();
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) => {
497 this._refreshProcess = null;
498 this.noResults.hidden = this.numVisibleProperties > 0;
499 this.inspector.emit("computed-view-refreshed");
500 deferred.resolve(undefined);
503 this._refreshProcess.schedule();
504 return deferred.promise;
505 }).then(null, (err) => console.error(err));
509 * Called when the user enters a search term.
511 * @param {Event} aEvent the DOM Event object.
513 filterChanged: function CssHtmlTree_filterChanged(aEvent)
515 let win = this.styleWindow;
517 if (this._filterChangedTimeout) {
518 win.clearTimeout(this._filterChangedTimeout);
521 this._filterChangedTimeout = win.setTimeout(() => {
523 this._filterChangeTimeout = null;
524 }, FILTER_CHANGED_TIMEOUT);
528 * The change event handler for the includeBrowserStyles checkbox.
530 * @param {Event} aEvent the DOM Event object.
532 includeBrowserStylesChanged:
533 function CssHtmltree_includeBrowserStylesChanged(aEvent)
535 this.refreshSourceFilter();
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.
545 refreshSourceFilter: function CssHtmlTree_setSourceFilter()
547 this._matchedProperties = null;
548 this._sourceFilter = this.includeBrowserStyles ?
550 CssLogic.FILTER.USER;
553 _updateSourceLinks: function CssHtmlTree__updateSourceLinks()
555 for (let propView of this.propertyViews) {
556 propView.updateSourceLinks();
558 this.inspector.emit("computed-view-sourcelinks-updated");
562 * The CSS as displayed by the UI.
564 createStyleViews: function CssHtmlTree_createStyleViews()
566 if (CssHtmlTree.propertyNames) {
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);
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
581 } else if (prop.startsWith("-")) {
584 CssHtmlTree.propertyNames.push(prop);
588 CssHtmlTree.propertyNames.sort();
589 CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames,
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");
603 * Get a set of properties that have matched selectors.
605 * @return {Set} If a property name is in the set, it has matching selectors.
607 get matchedProperties()
609 return this._matchedProperties || new Set;
613 * Focus the window on mousedown.
615 * @param aEvent The event object
617 focusWindow: function(aEvent)
619 let win = this.styleDocument.defaultView;
624 * Create a context menu.
626 _buildContextMenu: function()
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";
635 this.menuitemSelectAll = createMenuItem(this._contextmenu, {
636 label: "computedView.contextmenu.selectAll",
637 accesskey: "computedView.contextmenu.selectAll.accessKey",
638 command: this._onSelectAll
642 this.menuitemCopy = createMenuItem(this._contextmenu, {
643 label: "computedView.contextmenu.copy",
644 accesskey: "computedView.contextmenu.copy.accessKey",
645 command: this._onCopy
649 this.menuitemCopyColor = createMenuItem(this._contextmenu, {
650 label: "ruleView.contextmenu.copyColor",
651 accesskey: "ruleView.contextmenu.copyColor.accessKey",
652 command: this._onCopyColor
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,
663 let popupset = doc.documentElement.querySelector("popupset");
665 popupset = doc.createElementNS(XUL_NS, "popupset");
666 doc.documentElement.appendChild(popupset);
668 popupset.appendChild(this._contextmenu);
672 * Update the context menu. This means enabling or disabling menuitems as
675 _contextMenuUpdate: function()
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();
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.
692 * true if click on color opened the popup, false otherwise.
694 _isColorPopup: function () {
695 this._colorToCopy = "";
697 let trigger = this.popupNode;
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;
714 this._colorToCopy = container.dataset["color"];
719 * Context menu handler.
721 _onContextMenu: function(event) {
723 this.popupNode = event.explicitOriginalTarget;
724 this.styleDocument.defaultView.focus();
725 this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
734 _onSelectAll: function()
737 let win = this.styleDocument.defaultView;
738 let selection = win.getSelection();
740 selection.selectAllChildren(this.styleDocument.documentElement);
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");
757 _onCopyColor: function() {
758 clipboardHelper.copyString(this._colorToCopy, this.styleDocument);
762 * Copy selected text.
764 * @param event The event object
766 _onCopy: function(event)
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]+/);
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) {
785 result += ": " + prop;
786 if (result.length > 0) {
792 // Short text fragment.
793 result = textArray[0];
796 clipboardHelper.copyString(result, this.styleDocument);
799 event.preventDefault();
807 * Toggle the original sources pref.
809 _onToggleOrigSources: function()
811 let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
812 Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
816 * Destructor for CssHtmlTree.
818 destroy: function CssHtmlTree_destroy()
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();
836 if (this._refreshProcess) {
837 this._refreshProcess.cancel();
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;
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
877 // The document in which we display the results (csshtmltree.xul).
878 this.styleDocument = null;
880 for (let propView of this.propertyViews) {
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;
892 function PropertyInfo(aTree, aName) {
896 PropertyInfo.prototype = {
898 if (this.tree._computed) {
899 let value = this.tree._computed[this.name].value;
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);
919 * A container to give easy access to property data from the template engine.
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.
926 function PropertyView(aTree, 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
942 // Property header node
943 propertyHeader: null,
945 // Destination for property names
948 // Destination for property values
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,
967 * Get the computed style for the current property.
969 * @return {string} the computed style for the current property of the
970 * currently highlighted element.
974 return this.propertyInfo.value;
978 * An easy way to access the CssPropertyInfo behind this PropertyView.
982 return this._propertyInfo;
986 * Does the property have any matched selectors?
988 get hasMatchedSelectors()
990 return this.tree.matchedProperties.has(this.name);
994 * Should this property be visible?
998 if (!this.tree.viewedElement) {
1002 if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
1006 let searchTerm = this.tree.searchField.value.toLowerCase();
1007 if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 &&
1008 this.value.toLowerCase().indexOf(searchTerm) == -1) {
1016 * Returns the className that should be assigned to the propertyView.
1019 get propertyHeaderClassName()
1022 let isDark = this.tree._darkStripe = !this.tree._darkStripe;
1023 return isDark ? "property-view row-striped" : "property-view";
1025 return "property-view-hidden";
1029 * Returns the className that should be assigned to the propertyView content
1033 get propertyContentClassName()
1036 let isDark = this.tree._darkStripe;
1037 return isDark ? "property-content row-striped" : "property-content";
1039 return "property-content-hidden";
1043 * Build the markup for on computed style
1046 buildMain: function PropertyView_buildMain()
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();
1063 if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
1064 aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
1065 this.onMatchedToggle(aEvent);
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;
1104 buildSelectorContainer: function PropertyView_buildSelectorContainer()
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);
1117 * Refresh the panel's CSS property value.
1119 refresh: function PropertyView_refresh()
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;
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");
1137 this.tree.numVisibleProperties++;
1139 let outputParser = this.tree._outputParser;
1140 let frag = outputParser.parseCssProperty(this.propertyInfo.name,
1141 this.propertyInfo.value,
1143 colorSwatchClass: "computedview-colorswatch",
1144 urlClass: "theme-link"
1145 // No need to use baseURI here as computed URIs are never relative.
1147 this.valueNode.innerHTML = "";
1148 this.valueNode.appendChild(frag);
1150 this.refreshMatchedSelectors();
1154 * Refresh the panel matched rules.
1156 refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
1158 let hasMatchedSelectors = this.hasMatchedSelectors;
1159 this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
1161 if (hasMatchedSelectors) {
1162 this.matchedExpander.classList.add("expandable");
1164 this.matchedExpander.classList.remove("expandable");
1167 if (this.matchedExpanded && hasMatchedSelectors) {
1168 return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => {
1169 if (!this.matchedExpanded) {
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);
1180 this.matchedSelectorsContainer.innerHTML = "";
1181 this.matchedExpander.removeAttribute("open");
1182 this.tree.inspector.emit("computed-view-property-collapsed");
1183 return promise.resolve(undefined);
1187 get matchedSelectors()
1189 return this._matchedSelectorResponse;
1193 * Provide access to the matched SelectorViews that we are currently
1196 get matchedSelectorViews()
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));
1206 return this._matchedSelectorViews;
1210 * Update all the selector source links to reflect whether we're linking to
1211 * original sources (e.g. Sass files).
1213 updateSourceLinks: function PropertyView_updateSourceLinks()
1215 if (!this._matchedSelectorViews) {
1218 for (let view of this._matchedSelectorViews) {
1219 view.updateSourceLink();
1224 * The action when a user expands matched selectors.
1226 * @param {Event} aEvent Used to determine the class name of the targets click
1229 onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
1231 this.matchedExpanded = !this.matchedExpanded;
1232 this.refreshMatchedSelectors();
1233 aEvent.preventDefault();
1237 * The action when a user clicks on the MDN help link for a property.
1239 mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
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");
1247 aEvent.preventDefault();
1251 * Destroy this property view, removing event listeners
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;
1270 * A container to give us easy access to display data from a CssRule
1271 * @param CssHtmlTree aTree, the owning CssHtmlTree
1272 * @param aSelectorInfo
1274 function SelectorView(aTree, aSelectorInfo)
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
1288 SelectorView.STATUS_NAMES = [
1289 // "Parent Match", "Matched", "Best Match"
1292 SelectorView.CLASS_NAMES = [
1293 "parentmatch", "matched", "bestmatch"
1296 SelectorView.prototype = {
1298 * Cache localized status names.
1300 * These statuses are localized inside the styleinspector.properties string
1302 * @see css-logic.js - the CssLogic.STATUS array.
1306 _cacheStatusNames: function SelectorView_cacheStatusNames()
1308 if (SelectorView.STATUS_NAMES.length) {
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');
1323 * A localized version of cssRule.status
1327 return SelectorView.STATUS_NAMES[this.selectorInfo.status];
1331 * Get class name for selector depending on status
1335 return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
1343 let sheet = this.selectorInfo.rule.parentStyleSheet;
1344 this._href = sheet ? sheet.href : "#";
1350 return this.selectorInfo.sourceText;
1356 return this.selectorInfo.value;
1359 get outputFragment()
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
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
1377 * Update the text of the source link to reflect whether we're showing
1378 * original sources or not.
1380 updateSourceLink: function()
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);
1387 link.textContent = this.source;
1388 link.setAttribute("sourcelocation", this.source);
1395 * Update the 'source' store based on our original sources preference.
1397 updateSource: function()
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");
1406 return promise.resolve(oldSource);
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);
1423 return deferred.promise;
1426 let oldSource = this.source;
1427 this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
1428 return promise.resolve(oldSource);
1432 * Open the style editor if the RETURN key was pressed.
1434 maybeOpenStyleEditor: function(aEvent)
1436 let keyEvent = Ci.nsIDOMKeyEvent;
1437 if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
1438 this.openStyleEditor();
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.
1447 * We can only view stylesheets contained in document.styleSheets inside the
1450 * @param aEvent The click event
1452 openStyleEditor: function(aEvent)
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.
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();
1468 contentDoc = rawNode.ownerDocument;
1471 let viewSourceUtils = inspector.viewSourceUtils;
1472 viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line);
1476 let location = promise.resolve(rule.location);
1477 if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
1478 location = rule.getOriginalLocation();
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);
1492 exports.CssHtmlTree = CssHtmlTree;
1493 exports.PropertyView = PropertyView;