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