browser/devtools/styleinspector/computed-view.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial