michael@0: /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: michael@0: const ToolDefinitions = require("main").Tools; michael@0: const {CssLogic} = require("devtools/styleinspector/css-logic"); michael@0: const {ELEMENT_STYLE} = require("devtools/server/actors/styles"); michael@0: const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const {EventEmitter} = require("devtools/toolkit/event-emitter"); michael@0: const {OutputParser} = require("devtools/output-parser"); michael@0: const {Tooltip} = require("devtools/shared/widgets/Tooltip"); michael@0: const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils"); michael@0: const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/Templater.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", michael@0: "resource://gre/modules/PluralForm.jsm"); michael@0: michael@0: const FILTER_CHANGED_TIMEOUT = 300; michael@0: const HTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; michael@0: michael@0: /** michael@0: * Helper for long-running processes that should yield occasionally to michael@0: * the mainloop. michael@0: * michael@0: * @param {Window} aWin michael@0: * Timeouts will be set on this window when appropriate. michael@0: * @param {Generator} aGenerator michael@0: * Will iterate this generator. michael@0: * @param {object} aOptions michael@0: * Options for the update process: michael@0: * onItem {function} Will be called with the value of each iteration. michael@0: * onBatch {function} Will be called after each batch of iterations, michael@0: * before yielding to the main loop. michael@0: * onDone {function} Will be called when iteration is complete. michael@0: * onCancel {function} Will be called if the process is canceled. michael@0: * threshold {int} How long to process before yielding, in ms. michael@0: * michael@0: * @constructor michael@0: */ michael@0: function UpdateProcess(aWin, aGenerator, aOptions) michael@0: { michael@0: this.win = aWin; michael@0: this.iter = _Iterator(aGenerator); michael@0: this.onItem = aOptions.onItem || function() {}; michael@0: this.onBatch = aOptions.onBatch || function () {}; michael@0: this.onDone = aOptions.onDone || function() {}; michael@0: this.onCancel = aOptions.onCancel || function() {}; michael@0: this.threshold = aOptions.threshold || 45; michael@0: michael@0: this.canceled = false; michael@0: } michael@0: michael@0: UpdateProcess.prototype = { michael@0: /** michael@0: * Schedule a new batch on the main loop. michael@0: */ michael@0: schedule: function UP_schedule() michael@0: { michael@0: if (this.canceled) { michael@0: return; michael@0: } michael@0: this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0); michael@0: }, michael@0: michael@0: /** michael@0: * Cancel the running process. onItem will not be called again, michael@0: * and onCancel will be called. michael@0: */ michael@0: cancel: function UP_cancel() michael@0: { michael@0: if (this._timeout) { michael@0: this.win.clearTimeout(this._timeout); michael@0: this._timeout = 0; michael@0: } michael@0: this.canceled = true; michael@0: this.onCancel(); michael@0: }, michael@0: michael@0: _timeoutHandler: function UP_timeoutHandler() { michael@0: this._timeout = null; michael@0: try { michael@0: this._runBatch(); michael@0: this.schedule(); michael@0: } catch(e) { michael@0: if (e instanceof StopIteration) { michael@0: this.onBatch(); michael@0: this.onDone(); michael@0: return; michael@0: } michael@0: console.error(e); michael@0: throw e; michael@0: } michael@0: }, michael@0: michael@0: _runBatch: function Y_runBatch() michael@0: { michael@0: let time = Date.now(); michael@0: while(!this.canceled) { michael@0: // Continue until iter.next() throws... michael@0: let next = this.iter.next(); michael@0: this.onItem(next[1]); michael@0: if ((Date.now() - time) > this.threshold) { michael@0: this.onBatch(); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * CssHtmlTree is a panel that manages the display of a table sorted by style. michael@0: * There should be one instance of CssHtmlTree per style display (of which there michael@0: * will generally only be one). michael@0: * michael@0: * @params {StyleInspector} aStyleInspector The owner of this CssHtmlTree michael@0: * @param {PageStyleFront} aPageStyle michael@0: * Front for the page style actor that will be providing michael@0: * the style information. michael@0: * michael@0: * @constructor michael@0: */ michael@0: function CssHtmlTree(aStyleInspector, aPageStyle) michael@0: { michael@0: this.styleWindow = aStyleInspector.window; michael@0: this.styleDocument = aStyleInspector.window.document; michael@0: this.styleInspector = aStyleInspector; michael@0: this.pageStyle = aPageStyle; michael@0: this.propertyViews = []; michael@0: michael@0: this._outputParser = new OutputParser(); michael@0: michael@0: let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. michael@0: getService(Ci.nsIXULChromeRegistry); michael@0: this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr"; michael@0: michael@0: // Create bound methods. michael@0: this.focusWindow = this.focusWindow.bind(this); michael@0: this._onContextMenu = this._onContextMenu.bind(this); michael@0: this._contextMenuUpdate = this._contextMenuUpdate.bind(this); michael@0: this._onSelectAll = this._onSelectAll.bind(this); michael@0: this._onClick = this._onClick.bind(this); michael@0: this._onCopy = this._onCopy.bind(this); michael@0: michael@0: this.styleDocument.addEventListener("copy", this._onCopy); michael@0: this.styleDocument.addEventListener("mousedown", this.focusWindow); michael@0: this.styleDocument.addEventListener("contextmenu", this._onContextMenu); michael@0: michael@0: // Nodes used in templating michael@0: this.root = this.styleDocument.getElementById("root"); michael@0: this.templateRoot = this.styleDocument.getElementById("templateRoot"); michael@0: this.propertyContainer = this.styleDocument.getElementById("propertyContainer"); michael@0: michael@0: // Listen for click events michael@0: this.propertyContainer.addEventListener("click", this._onClick, false); michael@0: michael@0: // No results text. michael@0: this.noResults = this.styleDocument.getElementById("noResults"); michael@0: michael@0: // Refresh panel when color unit changed. michael@0: this._handlePrefChange = this._handlePrefChange.bind(this); michael@0: gDevTools.on("pref-changed", this._handlePrefChange); michael@0: michael@0: // Refresh panel when pref for showing original sources changes michael@0: this._updateSourceLinks = this._updateSourceLinks.bind(this); michael@0: this._prefObserver = new PrefObserver("devtools."); michael@0: this._prefObserver.on(PREF_ORIG_SOURCES, this._updateSourceLinks); michael@0: michael@0: CssHtmlTree.processTemplate(this.templateRoot, this.root, this); michael@0: michael@0: // The element that we're inspecting, and the document that it comes from. michael@0: this.viewedElement = null; michael@0: michael@0: // Properties preview tooltip michael@0: this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc); michael@0: this.tooltip.startTogglingOnHover(this.propertyContainer, michael@0: this._onTooltipTargetHover.bind(this)); michael@0: michael@0: this._buildContextMenu(); michael@0: this.createStyleViews(); michael@0: } michael@0: michael@0: /** michael@0: * Memoized lookup of a l10n string from a string bundle. michael@0: * @param {string} aName The key to lookup. michael@0: * @returns A localized version of the given key. michael@0: */ michael@0: CssHtmlTree.l10n = function CssHtmlTree_l10n(aName) michael@0: { michael@0: try { michael@0: return CssHtmlTree._strings.GetStringFromName(aName); michael@0: } catch (ex) { michael@0: Services.console.logStringMessage("Error reading '" + aName + "'"); michael@0: throw new Error("l10n error with " + aName); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Clone the given template node, and process it by resolving ${} references michael@0: * in the template. michael@0: * michael@0: * @param {nsIDOMElement} aTemplate the template note to use. michael@0: * @param {nsIDOMElement} aDestination the destination node where the michael@0: * processed nodes will be displayed. michael@0: * @param {object} aData the data to pass to the template. michael@0: * @param {Boolean} aPreserveDestination If true then the template will be michael@0: * appended to aDestination's content else aDestination.innerHTML will be michael@0: * cleared before the template is appended. michael@0: */ michael@0: CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate, michael@0: aDestination, aData, aPreserveDestination) michael@0: { michael@0: if (!aPreserveDestination) { michael@0: aDestination.innerHTML = ""; michael@0: } michael@0: michael@0: // All the templater does is to populate a given DOM tree with the given michael@0: // values, so we need to clone the template first. michael@0: let duplicated = aTemplate.cloneNode(true); michael@0: michael@0: // See https://github.com/mozilla/domtemplate/blob/master/README.md michael@0: // for docs on the template() function michael@0: template(duplicated, aData, { allowEval: true }); michael@0: while (duplicated.firstChild) { michael@0: aDestination.appendChild(duplicated.firstChild); michael@0: } michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings michael@0: .createBundle("chrome://global/locale/devtools/styleinspector.properties")); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { michael@0: return Cc["@mozilla.org/widget/clipboardhelper;1"]. michael@0: getService(Ci.nsIClipboardHelper); michael@0: }); michael@0: michael@0: CssHtmlTree.prototype = { michael@0: // Cache the list of properties that match the selected element. michael@0: _matchedProperties: null, michael@0: michael@0: // Used for cancelling timeouts in the style filter. michael@0: _filterChangedTimeout: null, michael@0: michael@0: // The search filter michael@0: searchField: null, michael@0: michael@0: // Reference to the "Include browser styles" checkbox. michael@0: includeBrowserStylesCheckbox: null, michael@0: michael@0: // Holds the ID of the panelRefresh timeout. michael@0: _panelRefreshTimeout: null, michael@0: michael@0: // Toggle for zebra striping michael@0: _darkStripe: true, michael@0: michael@0: // Number of visible properties michael@0: numVisibleProperties: 0, michael@0: michael@0: setPageStyle: function(pageStyle) { michael@0: this.pageStyle = pageStyle; michael@0: }, michael@0: michael@0: get includeBrowserStyles() michael@0: { michael@0: return this.includeBrowserStylesCheckbox.checked; michael@0: }, michael@0: michael@0: _handlePrefChange: function(event, data) { michael@0: if (this._computed && (data.pref == "devtools.defaultColorUnit" || michael@0: data.pref == PREF_ORIG_SOURCES)) { michael@0: this.refreshPanel(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Update the highlighted element. The CssHtmlTree panel will show the style michael@0: * information for the given element. michael@0: * @param {nsIDOMElement} aElement The highlighted node to get styles for. michael@0: * michael@0: * @returns a promise that will be resolved when highlighting is complete. michael@0: */ michael@0: highlight: function(aElement) { michael@0: if (!aElement) { michael@0: this.viewedElement = null; michael@0: this.noResults.hidden = false; michael@0: michael@0: if (this._refreshProcess) { michael@0: this._refreshProcess.cancel(); michael@0: } michael@0: // Hiding all properties michael@0: for (let propView of this.propertyViews) { michael@0: propView.refresh(); michael@0: } michael@0: return promise.resolve(undefined); michael@0: } michael@0: michael@0: this.tooltip.hide(); michael@0: michael@0: if (aElement === this.viewedElement) { michael@0: return promise.resolve(undefined); michael@0: } michael@0: michael@0: this.viewedElement = aElement; michael@0: this.refreshSourceFilter(); michael@0: michael@0: return this.refreshPanel(); michael@0: }, michael@0: michael@0: _createPropertyViews: function() michael@0: { michael@0: if (this._createViewsPromise) { michael@0: return this._createViewsPromise; michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: this._createViewsPromise = deferred.promise; michael@0: michael@0: this.refreshSourceFilter(); michael@0: this.numVisibleProperties = 0; michael@0: let fragment = this.styleDocument.createDocumentFragment(); michael@0: michael@0: this._createViewsProcess = new UpdateProcess(this.styleWindow, CssHtmlTree.propertyNames, { michael@0: onItem: (aPropertyName) => { michael@0: // Per-item callback. michael@0: let propView = new PropertyView(this, aPropertyName); michael@0: fragment.appendChild(propView.buildMain()); michael@0: fragment.appendChild(propView.buildSelectorContainer()); michael@0: michael@0: if (propView.visible) { michael@0: this.numVisibleProperties++; michael@0: } michael@0: this.propertyViews.push(propView); michael@0: }, michael@0: onCancel: () => { michael@0: deferred.reject("_createPropertyViews cancelled"); michael@0: }, michael@0: onDone: () => { michael@0: // Completed callback. michael@0: this.propertyContainer.appendChild(fragment); michael@0: this.noResults.hidden = this.numVisibleProperties > 0; michael@0: deferred.resolve(undefined); michael@0: } michael@0: }); michael@0: michael@0: this._createViewsProcess.schedule(); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Refresh the panel content. michael@0: */ michael@0: refreshPanel: function CssHtmlTree_refreshPanel() michael@0: { michael@0: if (!this.viewedElement) { michael@0: return promise.resolve(); michael@0: } michael@0: michael@0: return promise.all([ michael@0: this._createPropertyViews(), michael@0: this.pageStyle.getComputed(this.viewedElement, { michael@0: filter: this._sourceFilter, michael@0: onlyMatched: !this.includeBrowserStyles, michael@0: markMatched: true michael@0: }) michael@0: ]).then(([createViews, computed]) => { michael@0: this._matchedProperties = new Set; michael@0: for (let name in computed) { michael@0: if (computed[name].matched) { michael@0: this._matchedProperties.add(name); michael@0: } michael@0: } michael@0: this._computed = computed; michael@0: michael@0: if (this._refreshProcess) { michael@0: this._refreshProcess.cancel(); michael@0: } michael@0: michael@0: this.noResults.hidden = true; michael@0: michael@0: // Reset visible property count michael@0: this.numVisibleProperties = 0; michael@0: michael@0: // Reset zebra striping. michael@0: this._darkStripe = true; michael@0: michael@0: let deferred = promise.defer(); michael@0: this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, { michael@0: onItem: (aPropView) => { michael@0: aPropView.refresh(); michael@0: }, michael@0: onDone: () => { michael@0: this._refreshProcess = null; michael@0: this.noResults.hidden = this.numVisibleProperties > 0; michael@0: this.styleInspector.inspector.emit("computed-view-refreshed"); michael@0: deferred.resolve(undefined); michael@0: } michael@0: }); michael@0: this._refreshProcess.schedule(); michael@0: return deferred.promise; michael@0: }).then(null, (err) => console.error(err)); michael@0: }, michael@0: michael@0: /** michael@0: * Called when the user enters a search term. michael@0: * michael@0: * @param {Event} aEvent the DOM Event object. michael@0: */ michael@0: filterChanged: function CssHtmlTree_filterChanged(aEvent) michael@0: { michael@0: let win = this.styleWindow; michael@0: michael@0: if (this._filterChangedTimeout) { michael@0: win.clearTimeout(this._filterChangedTimeout); michael@0: } michael@0: michael@0: this._filterChangedTimeout = win.setTimeout(function() { michael@0: this.refreshPanel(); michael@0: this._filterChangeTimeout = null; michael@0: }.bind(this), FILTER_CHANGED_TIMEOUT); michael@0: }, michael@0: michael@0: /** michael@0: * The change event handler for the includeBrowserStyles checkbox. michael@0: * michael@0: * @param {Event} aEvent the DOM Event object. michael@0: */ michael@0: includeBrowserStylesChanged: michael@0: function CssHtmltree_includeBrowserStylesChanged(aEvent) michael@0: { michael@0: this.refreshSourceFilter(); michael@0: this.refreshPanel(); michael@0: }, michael@0: michael@0: /** michael@0: * When includeBrowserStyles.checked is false we only display properties that michael@0: * have matched selectors and have been included by the document or one of the michael@0: * document's stylesheets. If .checked is false we display all properties michael@0: * including those that come from UA stylesheets. michael@0: */ michael@0: refreshSourceFilter: function CssHtmlTree_setSourceFilter() michael@0: { michael@0: this._matchedProperties = null; michael@0: this._sourceFilter = this.includeBrowserStyles ? michael@0: CssLogic.FILTER.UA : michael@0: CssLogic.FILTER.USER; michael@0: }, michael@0: michael@0: _updateSourceLinks: function CssHtmlTree__updateSourceLinks() michael@0: { michael@0: for (let propView of this.propertyViews) { michael@0: propView.updateSourceLinks(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The CSS as displayed by the UI. michael@0: */ michael@0: createStyleViews: function CssHtmlTree_createStyleViews() michael@0: { michael@0: if (CssHtmlTree.propertyNames) { michael@0: return; michael@0: } michael@0: michael@0: CssHtmlTree.propertyNames = []; michael@0: michael@0: // Here we build and cache a list of css properties supported by the browser michael@0: // We could use any element but let's use the main document's root element michael@0: let styles = this.styleWindow.getComputedStyle(this.styleDocument.documentElement); michael@0: let mozProps = []; michael@0: for (let i = 0, numStyles = styles.length; i < numStyles; i++) { michael@0: let prop = styles.item(i); michael@0: if (prop.charAt(0) == "-") { michael@0: mozProps.push(prop); michael@0: } else { michael@0: CssHtmlTree.propertyNames.push(prop); michael@0: } michael@0: } michael@0: michael@0: CssHtmlTree.propertyNames.sort(); michael@0: CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames, michael@0: mozProps.sort()); michael@0: michael@0: this._createPropertyViews(); michael@0: }, michael@0: michael@0: /** michael@0: * Get a set of properties that have matched selectors. michael@0: * michael@0: * @return {Set} If a property name is in the set, it has matching selectors. michael@0: */ michael@0: get matchedProperties() michael@0: { michael@0: return this._matchedProperties || new Set; michael@0: }, michael@0: michael@0: /** michael@0: * Focus the window on mousedown. michael@0: * michael@0: * @param aEvent The event object michael@0: */ michael@0: focusWindow: function(aEvent) michael@0: { michael@0: let win = this.styleDocument.defaultView; michael@0: win.focus(); michael@0: }, michael@0: michael@0: /** michael@0: * Executed by the tooltip when the pointer hovers over an element of the view. michael@0: * Used to decide whether the tooltip should be shown or not and to actually michael@0: * put content in it. michael@0: * Checks if the hovered target is a css value we support tooltips for. michael@0: */ michael@0: _onTooltipTargetHover: function(target) michael@0: { michael@0: let inspector = this.styleInspector.inspector; michael@0: michael@0: // Test for image url michael@0: if (target.classList.contains("theme-link") && inspector.hasUrlToImageDataResolver) { michael@0: let propValue = target.parentNode; michael@0: let propName = propValue.parentNode.querySelector(".property-name"); michael@0: if (propName.textContent === "background-image") { michael@0: let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize"); michael@0: let uri = CssLogic.getBackgroundImageUriFromProperty(propValue.textContent); michael@0: return this.tooltip.setRelativeImageContent(uri, inspector.inspector, maxDim); michael@0: } michael@0: } michael@0: michael@0: if (target.classList.contains("property-value")) { michael@0: let propValue = target; michael@0: let propName = target.parentNode.querySelector(".property-name"); michael@0: michael@0: // Test for css transform michael@0: if (propName.textContent === "transform") { michael@0: return this.tooltip.setCssTransformContent(propValue.textContent, michael@0: this.pageStyle, this.viewedElement); michael@0: } michael@0: michael@0: // Test for font family michael@0: if (propName.textContent === "font-family") { michael@0: this.tooltip.setFontFamilyContent(propValue.textContent); michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: // If the target isn't one that should receive a tooltip, signal it by rejecting michael@0: // a promise michael@0: return promise.reject(); michael@0: }, michael@0: michael@0: /** michael@0: * Create a context menu. michael@0: */ michael@0: _buildContextMenu: function() michael@0: { michael@0: let doc = this.styleDocument.defaultView.parent.document; michael@0: michael@0: this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup"); michael@0: this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate); michael@0: this._contextmenu.id = "computed-view-context-menu"; michael@0: michael@0: // Select All michael@0: this.menuitemSelectAll = createMenuItem(this._contextmenu, { michael@0: label: "computedView.contextmenu.selectAll", michael@0: accesskey: "computedView.contextmenu.selectAll.accessKey", michael@0: command: this._onSelectAll michael@0: }); michael@0: michael@0: // Copy michael@0: this.menuitemCopy = createMenuItem(this._contextmenu, { michael@0: label: "computedView.contextmenu.copy", michael@0: accesskey: "computedView.contextmenu.copy.accessKey", michael@0: command: this._onCopy michael@0: }); michael@0: michael@0: // Show Original Sources michael@0: this.menuitemSources= createMenuItem(this._contextmenu, { michael@0: label: "ruleView.contextmenu.showOrigSources", michael@0: accesskey: "ruleView.contextmenu.showOrigSources.accessKey", michael@0: command: this._onToggleOrigSources michael@0: }); michael@0: michael@0: let popupset = doc.documentElement.querySelector("popupset"); michael@0: if (!popupset) { michael@0: popupset = doc.createElementNS(XUL_NS, "popupset"); michael@0: doc.documentElement.appendChild(popupset); michael@0: } michael@0: popupset.appendChild(this._contextmenu); michael@0: }, michael@0: michael@0: /** michael@0: * Update the context menu. This means enabling or disabling menuitems as michael@0: * appropriate. michael@0: */ michael@0: _contextMenuUpdate: function() michael@0: { michael@0: let win = this.styleDocument.defaultView; michael@0: let disable = win.getSelection().isCollapsed; michael@0: this.menuitemCopy.disabled = disable; michael@0: michael@0: let label = "ruleView.contextmenu.showOrigSources"; michael@0: if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { michael@0: label = "ruleView.contextmenu.showCSSSources"; michael@0: } michael@0: this.menuitemSources.setAttribute("label", michael@0: CssHtmlTree.l10n(label)); michael@0: michael@0: let accessKey = label + ".accessKey"; michael@0: this.menuitemSources.setAttribute("accesskey", michael@0: CssHtmlTree.l10n(accessKey)); michael@0: }, michael@0: michael@0: /** michael@0: * Context menu handler. michael@0: */ michael@0: _onContextMenu: function(event) { michael@0: try { michael@0: this.styleDocument.defaultView.focus(); michael@0: this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true); michael@0: } catch(e) { michael@0: console.error(e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Select all text. michael@0: */ michael@0: _onSelectAll: function() michael@0: { michael@0: try { michael@0: let win = this.styleDocument.defaultView; michael@0: let selection = win.getSelection(); michael@0: michael@0: selection.selectAllChildren(this.styleDocument.documentElement); michael@0: } catch(e) { michael@0: console.error(e); michael@0: } michael@0: }, michael@0: michael@0: _onClick: function(event) { michael@0: let target = event.target; michael@0: michael@0: if (target.nodeName === "a") { michael@0: event.stopPropagation(); michael@0: event.preventDefault(); michael@0: let browserWin = this.styleInspector.inspector.target michael@0: .tab.ownerDocument.defaultView; michael@0: browserWin.openUILinkIn(target.href, "tab"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Copy selected text. michael@0: * michael@0: * @param event The event object michael@0: */ michael@0: _onCopy: function(event) michael@0: { michael@0: try { michael@0: let win = this.styleDocument.defaultView; michael@0: let text = win.getSelection().toString().trim(); michael@0: michael@0: // Tidy up block headings by moving CSS property names and their values onto michael@0: // the same line and inserting a colon between them. michael@0: let textArray = text.split(/[\r\n]+/); michael@0: let result = ""; michael@0: michael@0: // Parse text array to output string. michael@0: if (textArray.length > 1) { michael@0: for (let prop of textArray) { michael@0: if (CssHtmlTree.propertyNames.indexOf(prop) !== -1) { michael@0: // Property name michael@0: result += prop; michael@0: } else { michael@0: // Property value michael@0: result += ": " + prop; michael@0: if (result.length > 0) { michael@0: result += ";\n"; michael@0: } michael@0: } michael@0: } michael@0: } else { michael@0: // Short text fragment. michael@0: result = textArray[0]; michael@0: } michael@0: michael@0: clipboardHelper.copyString(result, this.styleDocument); michael@0: michael@0: if (event) { michael@0: event.preventDefault(); michael@0: } michael@0: } catch(e) { michael@0: console.error(e); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Toggle the original sources pref. michael@0: */ michael@0: _onToggleOrigSources: function() michael@0: { michael@0: let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); michael@0: Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); michael@0: }, michael@0: michael@0: /** michael@0: * Destructor for CssHtmlTree. michael@0: */ michael@0: destroy: function CssHtmlTree_destroy() michael@0: { michael@0: delete this.viewedElement; michael@0: delete this._outputParser; michael@0: michael@0: // Remove event listeners michael@0: this.includeBrowserStylesCheckbox.removeEventListener("command", michael@0: this.includeBrowserStylesChanged); michael@0: this.searchField.removeEventListener("command", this.filterChanged); michael@0: gDevTools.off("pref-changed", this._handlePrefChange); michael@0: michael@0: this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks); michael@0: this._prefObserver.destroy(); michael@0: michael@0: // Cancel tree construction michael@0: if (this._createViewsProcess) { michael@0: this._createViewsProcess.cancel(); michael@0: } michael@0: if (this._refreshProcess) { michael@0: this._refreshProcess.cancel(); michael@0: } michael@0: michael@0: this.propertyContainer.removeEventListener("click", this._onClick, false); michael@0: michael@0: // Remove context menu michael@0: if (this._contextmenu) { michael@0: // Destroy the Select All menuitem. michael@0: this.menuitemCopy.removeEventListener("command", this._onCopy); michael@0: this.menuitemCopy = null; michael@0: michael@0: // Destroy the Copy menuitem. michael@0: this.menuitemSelectAll.removeEventListener("command", this._onSelectAll); michael@0: this.menuitemSelectAll = null; michael@0: michael@0: // Destroy the context menu. michael@0: this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate); michael@0: this._contextmenu.parentNode.removeChild(this._contextmenu); michael@0: this._contextmenu = null; michael@0: } michael@0: michael@0: this.tooltip.stopTogglingOnHover(this.propertyContainer); michael@0: this.tooltip.destroy(); michael@0: michael@0: // Remove bound listeners michael@0: this.styleDocument.removeEventListener("contextmenu", this._onContextMenu); michael@0: this.styleDocument.removeEventListener("copy", this._onCopy); michael@0: this.styleDocument.removeEventListener("mousedown", this.focusWindow); michael@0: michael@0: // Nodes used in templating michael@0: delete this.root; michael@0: delete this.propertyContainer; michael@0: delete this.panel; michael@0: michael@0: // The document in which we display the results (csshtmltree.xul). michael@0: delete this.styleDocument; michael@0: michael@0: for (let propView of this.propertyViews) { michael@0: propView.destroy(); michael@0: } michael@0: michael@0: // The element that we're inspecting, and the document that it comes from. michael@0: delete this.propertyViews; michael@0: delete this.styleWindow; michael@0: delete this.styleDocument; michael@0: delete this.styleInspector; michael@0: } michael@0: }; michael@0: michael@0: function PropertyInfo(aTree, aName) { michael@0: this.tree = aTree; michael@0: this.name = aName; michael@0: } michael@0: PropertyInfo.prototype = { michael@0: get value() { michael@0: if (this.tree._computed) { michael@0: let value = this.tree._computed[this.name].value; michael@0: return value; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: function createMenuItem(aMenu, aAttributes) michael@0: { michael@0: let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem"); michael@0: michael@0: item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label)); michael@0: item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey)); michael@0: item.addEventListener("command", aAttributes.command); michael@0: michael@0: aMenu.appendChild(item); michael@0: michael@0: return item; michael@0: } michael@0: michael@0: /** michael@0: * A container to give easy access to property data from the template engine. michael@0: * michael@0: * @constructor michael@0: * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with. michael@0: * @param {string} aName the CSS property name for which this PropertyView michael@0: * instance will render the rules. michael@0: */ michael@0: function PropertyView(aTree, aName) michael@0: { michael@0: this.tree = aTree; michael@0: this.name = aName; michael@0: this.getRTLAttr = aTree.getRTLAttr; michael@0: michael@0: this.link = "https://developer.mozilla.org/CSS/" + aName; michael@0: michael@0: this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors"); michael@0: this._propertyInfo = new PropertyInfo(aTree, aName); michael@0: } michael@0: michael@0: PropertyView.prototype = { michael@0: // The parent element which contains the open attribute michael@0: element: null, michael@0: michael@0: // Property header node michael@0: propertyHeader: null, michael@0: michael@0: // Destination for property names michael@0: nameNode: null, michael@0: michael@0: // Destination for property values michael@0: valueNode: null, michael@0: michael@0: // Are matched rules expanded? michael@0: matchedExpanded: false, michael@0: michael@0: // Matched selector container michael@0: matchedSelectorsContainer: null, michael@0: michael@0: // Matched selector expando michael@0: matchedExpander: null, michael@0: michael@0: // Cache for matched selector views michael@0: _matchedSelectorViews: null, michael@0: michael@0: // The previously selected element used for the selector view caches michael@0: prevViewedElement: null, michael@0: michael@0: /** michael@0: * Get the computed style for the current property. michael@0: * michael@0: * @return {string} the computed style for the current property of the michael@0: * currently highlighted element. michael@0: */ michael@0: get value() michael@0: { michael@0: return this.propertyInfo.value; michael@0: }, michael@0: michael@0: /** michael@0: * An easy way to access the CssPropertyInfo behind this PropertyView. michael@0: */ michael@0: get propertyInfo() michael@0: { michael@0: return this._propertyInfo; michael@0: }, michael@0: michael@0: /** michael@0: * Does the property have any matched selectors? michael@0: */ michael@0: get hasMatchedSelectors() michael@0: { michael@0: return this.tree.matchedProperties.has(this.name); michael@0: }, michael@0: michael@0: /** michael@0: * Should this property be visible? michael@0: */ michael@0: get visible() michael@0: { michael@0: if (!this.tree.viewedElement) { michael@0: return false; michael@0: } michael@0: michael@0: if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) { michael@0: return false; michael@0: } michael@0: michael@0: let searchTerm = this.tree.searchField.value.toLowerCase(); michael@0: if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 && michael@0: this.value.toLowerCase().indexOf(searchTerm) == -1) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the className that should be assigned to the propertyView. michael@0: * @return string michael@0: */ michael@0: get propertyHeaderClassName() michael@0: { michael@0: if (this.visible) { michael@0: let isDark = this.tree._darkStripe = !this.tree._darkStripe; michael@0: return isDark ? "property-view theme-bg-darker" : "property-view"; michael@0: } michael@0: return "property-view-hidden"; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the className that should be assigned to the propertyView content michael@0: * container. michael@0: * @return string michael@0: */ michael@0: get propertyContentClassName() michael@0: { michael@0: if (this.visible) { michael@0: let isDark = this.tree._darkStripe; michael@0: return isDark ? "property-content theme-bg-darker" : "property-content"; michael@0: } michael@0: return "property-content-hidden"; michael@0: }, michael@0: michael@0: /** michael@0: * Build the markup for on computed style michael@0: * @return Element michael@0: */ michael@0: buildMain: function PropertyView_buildMain() michael@0: { michael@0: let doc = this.tree.styleDocument; michael@0: michael@0: // Build the container element michael@0: this.onMatchedToggle = this.onMatchedToggle.bind(this); michael@0: this.element = doc.createElementNS(HTML_NS, "div"); michael@0: this.element.setAttribute("class", this.propertyHeaderClassName); michael@0: this.element.addEventListener("dblclick", this.onMatchedToggle, false); michael@0: michael@0: // Make it keyboard navigable michael@0: this.element.setAttribute("tabindex", "0"); michael@0: this.onKeyDown = (aEvent) => { michael@0: let keyEvent = Ci.nsIDOMKeyEvent; michael@0: if (aEvent.keyCode == keyEvent.DOM_VK_F1) { michael@0: this.mdnLinkClick(); michael@0: } michael@0: if (aEvent.keyCode == keyEvent.DOM_VK_RETURN || michael@0: aEvent.keyCode == keyEvent.DOM_VK_SPACE) { michael@0: this.onMatchedToggle(aEvent); michael@0: } michael@0: }; michael@0: this.element.addEventListener("keydown", this.onKeyDown, false); michael@0: michael@0: // Build the twisty expand/collapse michael@0: this.matchedExpander = doc.createElementNS(HTML_NS, "div"); michael@0: this.matchedExpander.className = "expander theme-twisty"; michael@0: this.matchedExpander.addEventListener("click", this.onMatchedToggle, false); michael@0: this.element.appendChild(this.matchedExpander); michael@0: michael@0: this.focusElement = () => this.element.focus(); michael@0: michael@0: // Build the style name element michael@0: this.nameNode = doc.createElementNS(HTML_NS, "div"); michael@0: this.nameNode.setAttribute("class", "property-name theme-fg-color5"); michael@0: // Reset its tabindex attribute otherwise, if an ellipsis is applied michael@0: // it will be reachable via TABing michael@0: this.nameNode.setAttribute("tabindex", ""); michael@0: this.nameNode.textContent = this.nameNode.title = this.name; michael@0: // Make it hand over the focus to the container michael@0: this.onFocus = () => this.element.focus(); michael@0: this.nameNode.addEventListener("click", this.onFocus, false); michael@0: this.element.appendChild(this.nameNode); michael@0: michael@0: // Build the style value element michael@0: this.valueNode = doc.createElementNS(HTML_NS, "div"); michael@0: this.valueNode.setAttribute("class", "property-value theme-fg-color1"); michael@0: // Reset its tabindex attribute otherwise, if an ellipsis is applied michael@0: // it will be reachable via TABing michael@0: this.valueNode.setAttribute("tabindex", ""); michael@0: this.valueNode.setAttribute("dir", "ltr"); michael@0: // Make it hand over the focus to the container michael@0: this.valueNode.addEventListener("click", this.onFocus, false); michael@0: this.element.appendChild(this.valueNode); michael@0: michael@0: return this.element; michael@0: }, michael@0: michael@0: buildSelectorContainer: function PropertyView_buildSelectorContainer() michael@0: { michael@0: let doc = this.tree.styleDocument; michael@0: let element = doc.createElementNS(HTML_NS, "div"); michael@0: element.setAttribute("class", this.propertyContentClassName); michael@0: this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div"); michael@0: this.matchedSelectorsContainer.setAttribute("class", "matchedselectors"); michael@0: element.appendChild(this.matchedSelectorsContainer); michael@0: michael@0: return element; michael@0: }, michael@0: michael@0: /** michael@0: * Refresh the panel's CSS property value. michael@0: */ michael@0: refresh: function PropertyView_refresh() michael@0: { michael@0: this.element.className = this.propertyHeaderClassName; michael@0: this.element.nextElementSibling.className = this.propertyContentClassName; michael@0: michael@0: if (this.prevViewedElement != this.tree.viewedElement) { michael@0: this._matchedSelectorViews = null; michael@0: this.prevViewedElement = this.tree.viewedElement; michael@0: } michael@0: michael@0: if (!this.tree.viewedElement || !this.visible) { michael@0: this.valueNode.textContent = this.valueNode.title = ""; michael@0: this.matchedSelectorsContainer.parentNode.hidden = true; michael@0: this.matchedSelectorsContainer.textContent = ""; michael@0: this.matchedExpander.removeAttribute("open"); michael@0: return; michael@0: } michael@0: michael@0: this.tree.numVisibleProperties++; michael@0: michael@0: let outputParser = this.tree._outputParser; michael@0: let frag = outputParser.parseCssProperty(this.propertyInfo.name, michael@0: this.propertyInfo.value, michael@0: { michael@0: colorSwatchClass: "computedview-colorswatch", michael@0: urlClass: "theme-link" michael@0: // No need to use baseURI here as computed URIs are never relative. michael@0: }); michael@0: this.valueNode.innerHTML = ""; michael@0: this.valueNode.appendChild(frag); michael@0: michael@0: this.refreshMatchedSelectors(); michael@0: }, michael@0: michael@0: /** michael@0: * Refresh the panel matched rules. michael@0: */ michael@0: refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors() michael@0: { michael@0: let hasMatchedSelectors = this.hasMatchedSelectors; michael@0: this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors; michael@0: michael@0: if (hasMatchedSelectors) { michael@0: this.matchedExpander.classList.add("expandable"); michael@0: } else { michael@0: this.matchedExpander.classList.remove("expandable"); michael@0: } michael@0: michael@0: if (this.matchedExpanded && hasMatchedSelectors) { michael@0: return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => { michael@0: if (!this.matchedExpanded) { michael@0: return; michael@0: } michael@0: michael@0: this._matchedSelectorResponse = matched; michael@0: CssHtmlTree.processTemplate(this.templateMatchedSelectors, michael@0: this.matchedSelectorsContainer, this); michael@0: this.matchedExpander.setAttribute("open", ""); michael@0: this.tree.styleInspector.inspector.emit("computed-view-property-expanded"); michael@0: }).then(null, console.error); michael@0: } else { michael@0: this.matchedSelectorsContainer.innerHTML = ""; michael@0: this.matchedExpander.removeAttribute("open"); michael@0: this.tree.styleInspector.inspector.emit("computed-view-property-collapsed"); michael@0: return promise.resolve(undefined); michael@0: } michael@0: }, michael@0: michael@0: get matchedSelectors() michael@0: { michael@0: return this._matchedSelectorResponse; michael@0: }, michael@0: michael@0: /** michael@0: * Provide access to the matched SelectorViews that we are currently michael@0: * displaying. michael@0: */ michael@0: get matchedSelectorViews() michael@0: { michael@0: if (!this._matchedSelectorViews) { michael@0: this._matchedSelectorViews = []; michael@0: this._matchedSelectorResponse.forEach( michael@0: function matchedSelectorViews_convert(aSelectorInfo) { michael@0: this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo)); michael@0: }, this); michael@0: } michael@0: michael@0: return this._matchedSelectorViews; michael@0: }, michael@0: michael@0: /** michael@0: * Update all the selector source links to reflect whether we're linking to michael@0: * original sources (e.g. Sass files). michael@0: */ michael@0: updateSourceLinks: function PropertyView_updateSourceLinks() michael@0: { michael@0: if (!this._matchedSelectorViews) { michael@0: return; michael@0: } michael@0: for (let view of this._matchedSelectorViews) { michael@0: view.updateSourceLink(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The action when a user expands matched selectors. michael@0: * michael@0: * @param {Event} aEvent Used to determine the class name of the targets click michael@0: * event. michael@0: */ michael@0: onMatchedToggle: function PropertyView_onMatchedToggle(aEvent) michael@0: { michael@0: this.matchedExpanded = !this.matchedExpanded; michael@0: this.refreshMatchedSelectors(); michael@0: aEvent.preventDefault(); michael@0: }, michael@0: michael@0: /** michael@0: * The action when a user clicks on the MDN help link for a property. michael@0: */ michael@0: mdnLinkClick: function PropertyView_mdnLinkClick(aEvent) michael@0: { michael@0: let inspector = this.tree.styleInspector.inspector; michael@0: michael@0: if (inspector.target.tab) { michael@0: let browserWin = inspector.target.tab.ownerDocument.defaultView; michael@0: browserWin.openUILinkIn(this.link, "tab"); michael@0: } michael@0: aEvent.preventDefault(); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy this property view, removing event listeners michael@0: */ michael@0: destroy: function PropertyView_destroy() { michael@0: this.element.removeEventListener("dblclick", this.onMatchedToggle, false); michael@0: this.element.removeEventListener("keydown", this.onKeyDown, false); michael@0: this.element = null; michael@0: michael@0: this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false); michael@0: this.matchedExpander = null; michael@0: michael@0: this.nameNode.removeEventListener("click", this.onFocus, false); michael@0: this.nameNode = null; michael@0: michael@0: this.valueNode.removeEventListener("click", this.onFocus, false); michael@0: this.valueNode = null; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * A container to give us easy access to display data from a CssRule michael@0: * @param CssHtmlTree aTree, the owning CssHtmlTree michael@0: * @param aSelectorInfo michael@0: */ michael@0: function SelectorView(aTree, aSelectorInfo) michael@0: { michael@0: this.tree = aTree; michael@0: this.selectorInfo = aSelectorInfo; michael@0: this._cacheStatusNames(); michael@0: michael@0: this.updateSourceLink(); michael@0: } michael@0: michael@0: /** michael@0: * Decode for cssInfo.rule.status michael@0: * @see SelectorView.prototype._cacheStatusNames michael@0: * @see CssLogic.STATUS michael@0: */ michael@0: SelectorView.STATUS_NAMES = [ michael@0: // "Parent Match", "Matched", "Best Match" michael@0: ]; michael@0: michael@0: SelectorView.CLASS_NAMES = [ michael@0: "parentmatch", "matched", "bestmatch" michael@0: ]; michael@0: michael@0: SelectorView.prototype = { michael@0: /** michael@0: * Cache localized status names. michael@0: * michael@0: * These statuses are localized inside the styleinspector.properties string michael@0: * bundle. michael@0: * @see css-logic.js - the CssLogic.STATUS array. michael@0: * michael@0: * @return {void} michael@0: */ michael@0: _cacheStatusNames: function SelectorView_cacheStatusNames() michael@0: { michael@0: if (SelectorView.STATUS_NAMES.length) { michael@0: return; michael@0: } michael@0: michael@0: for (let status in CssLogic.STATUS) { michael@0: let i = CssLogic.STATUS[status]; michael@0: if (i > CssLogic.STATUS.UNMATCHED) { michael@0: let value = CssHtmlTree.l10n("rule.status." + status); michael@0: // Replace normal spaces with non-breaking spaces michael@0: SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0'); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * A localized version of cssRule.status michael@0: */ michael@0: get statusText() michael@0: { michael@0: return SelectorView.STATUS_NAMES[this.selectorInfo.status]; michael@0: }, michael@0: michael@0: /** michael@0: * Get class name for selector depending on status michael@0: */ michael@0: get statusClass() michael@0: { michael@0: return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1]; michael@0: }, michael@0: michael@0: get href() michael@0: { michael@0: if (this._href) { michael@0: return this._href; michael@0: } michael@0: let sheet = this.selectorInfo.rule.parentStyleSheet; michael@0: this._href = sheet ? sheet.href : "#"; michael@0: return this._href; michael@0: }, michael@0: michael@0: get sourceText() michael@0: { michael@0: return this.selectorInfo.sourceText; michael@0: }, michael@0: michael@0: michael@0: get value() michael@0: { michael@0: return this.selectorInfo.value; michael@0: }, michael@0: michael@0: get outputFragment() michael@0: { michael@0: // Sadly, because this fragment is added to the template by DOM Templater michael@0: // we lose any events that are attached. This means that URLs will open in a michael@0: // new window. At some point we should fix this by stopping using the michael@0: // templater. michael@0: let outputParser = this.tree._outputParser; michael@0: let frag = outputParser.parseCssProperty( michael@0: this.selectorInfo.name, michael@0: this.selectorInfo.value, { michael@0: colorSwatchClass: "computedview-colorswatch", michael@0: urlClass: "theme-link", michael@0: baseURI: this.selectorInfo.rule.href michael@0: }); michael@0: return frag; michael@0: }, michael@0: michael@0: /** michael@0: * Update the text of the source link to reflect whether we're showing michael@0: * original sources or not. michael@0: */ michael@0: updateSourceLink: function() michael@0: { michael@0: this.updateSource().then((oldSource) => { michael@0: if (oldSource != this.source && this.tree.propertyContainer) { michael@0: let selector = '[sourcelocation="' + oldSource + '"]'; michael@0: let link = this.tree.propertyContainer.querySelector(selector); michael@0: if (link) { michael@0: link.textContent = this.source; michael@0: link.setAttribute("sourcelocation", this.source); michael@0: } michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Update the 'source' store based on our original sources preference. michael@0: */ michael@0: updateSource: function() michael@0: { michael@0: let rule = this.selectorInfo.rule; michael@0: this.sheet = rule.parentStyleSheet; michael@0: michael@0: if (!rule || !this.sheet) { michael@0: let oldSource = this.source; michael@0: this.source = CssLogic.l10n("rule.sourceElement"); michael@0: this.href = "#"; michael@0: return promise.resolve(oldSource); michael@0: } michael@0: michael@0: let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); michael@0: michael@0: if (showOrig && rule.type != ELEMENT_STYLE) { michael@0: let deferred = promise.defer(); michael@0: michael@0: // set as this first so we show something while we're fetching michael@0: this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; michael@0: michael@0: rule.getOriginalLocation().then(({href, line, column}) => { michael@0: let oldSource = this.source; michael@0: this.source = CssLogic.shortSource({href: href}) + ":" + line; michael@0: deferred.resolve(oldSource); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: let oldSource = this.source; michael@0: this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; michael@0: return promise.resolve(oldSource); michael@0: }, michael@0: michael@0: /** michael@0: * Open the style editor if the RETURN key was pressed. michael@0: */ michael@0: maybeOpenStyleEditor: function(aEvent) michael@0: { michael@0: let keyEvent = Ci.nsIDOMKeyEvent; michael@0: if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) { michael@0: this.openStyleEditor(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * When a css link is clicked this method is called in order to either: michael@0: * 1. Open the link in view source (for chrome stylesheets). michael@0: * 2. Open the link in the style editor. michael@0: * michael@0: * We can only view stylesheets contained in document.styleSheets inside the michael@0: * style editor. michael@0: * michael@0: * @param aEvent The click event michael@0: */ michael@0: openStyleEditor: function(aEvent) michael@0: { michael@0: let inspector = this.tree.styleInspector.inspector; michael@0: let rule = this.selectorInfo.rule; michael@0: michael@0: // The style editor can only display stylesheets coming from content because michael@0: // chrome stylesheets are not listed in the editor's stylesheet selector. michael@0: // michael@0: // If the stylesheet is a content stylesheet we send it to the style michael@0: // editor else we display it in the view source window. michael@0: let sheet = rule.parentStyleSheet; michael@0: if (!sheet || sheet.isSystem) { michael@0: let contentDoc = null; michael@0: if (this.tree.viewedElement.isLocal_toBeDeprecated()) { michael@0: let rawNode = this.tree.viewedElement.rawNode(); michael@0: if (rawNode) { michael@0: contentDoc = rawNode.ownerDocument; michael@0: } michael@0: } michael@0: let viewSourceUtils = inspector.viewSourceUtils; michael@0: viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line); michael@0: return; michael@0: } michael@0: michael@0: let location = promise.resolve({ michael@0: href: rule.href, michael@0: line: rule.line michael@0: }); michael@0: if (rule.href && Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { michael@0: location = rule.getOriginalLocation(); michael@0: } michael@0: michael@0: location.then(({href, line}) => { michael@0: let target = inspector.target; michael@0: if (ToolDefinitions.styleEditor.isTargetSupported(target)) { michael@0: gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) { michael@0: toolbox.getCurrentPanel().selectStyleSheet(href, line); michael@0: }); michael@0: } michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: exports.CssHtmlTree = CssHtmlTree; michael@0: exports.PropertyView = PropertyView;