Wed, 31 Dec 2014 06:09:35 +0100
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; |