/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/** * Helper for long-running processes that should yield occasionally to * the mainloop.
*/ class UpdateProcess { /** * @param {Window} win * Timeouts will be set on this window when appropriate. * @param {Array} array * The array of items to process. * @param {Object} options * Options for the update process: * onItem {function} Will be called with the value of each iteration. * onBatch {function} Will be called after each batch of iterations, * before yielding to the main loop. * onDone {function} Will be called when iteration is complete. * onCancel {function} Will be called if the process is canceled. * threshold {int} How long to process before yielding, in ms.
*/
constructor(win, array, options) { this.win = win; this.index = 0; this.array = array;
this.onItem = options.onItem || function () {}; this.onBatch = options.onBatch || function () {}; this.onDone = options.onDone || function () {}; this.onCancel = options.onCancel || function () {}; this.threshold = options.threshold || 45;
}
#canceled = false;
#timeout = null;
/** * Symbol returned when the array of items to process is empty.
*/ static ITERATION_DONE = Symbol("UpdateProcess iteration done");
/** * Schedule a new batch on the main loop.
*/
schedule() { if (this.#canceled) { return;
} this.#timeout = setTimeout(() => this.#timeoutHandler(), 0);
}
/** * Cancel the running process. onItem will not be called again, * and onCancel will be called.
*/
cancel() { if (this.#timeout) {
clearTimeout(this.#timeout); this.#timeout = null;
} this.#canceled = true; this.onCancel();
}
#runBatch() { const time = Date.now(); while (!this.#canceled) { const next = this.#next(); if (next === UpdateProcess.ITERATION_DONE) { return next;
}
this.onItem(next); if (Date.now() - time > this.threshold) { this.onBatch(); returnnull;
}
} returnnull;
}
/** * Returns the item at the current index and increases the index. * If all items have already been processed, will return ITERATION_DONE.
*/
#next() { if (this.index < this.array.length) { returnthis.array[this.index++];
} return UpdateProcess.ITERATION_DONE;
}
}
/** * CssComputedView is a panel that manages the display of a table * sorted by style. There should be one instance of CssComputedView * per style display (of which there will generally only be one).
*/ class CssComputedView { /** * @param {Inspector} inspector * Inspector toolbox panel * @param {Document} document * The document that will contain the computed view.
*/
constructor(inspector, document) { this.inspector = inspector; this.styleDocument = document; this.styleWindow = this.styleDocument.defaultView;
this.propertyViews = [];
this.#outputParser = new OutputParser(document, inspector.cssProperties);
if (flags.testing) { // In tests, we start listening immediately to avoid having to simulate a mousemove. this.highlighters.addToView(this);
} else { this.element.addEventListener( "mousemove",
() => { this.highlighters.addToView(this);
},
{ once: true, signal: this.#abortController.signal }
);
}
if (!this.inspector.is3PaneModeEnabled) { // When the rules view is added in 3 pane mode, refresh the Computed view whenever // the rules are changed. this.inspector.once( "ruleview-added",
() => { this.ruleView.on("ruleview-changed", this.refreshPanel, opts);
},
opts
);
}
if (this.ruleView) { this.ruleView.on("ruleview-changed", this.refreshPanel, opts);
}
this.searchClearButton.hidden = true;
// No results text. this.noResults = this.styleDocument.getElementById("computed-no-results");
// Refresh panel when color unit changed or pref for showing // original sources changes. this.#prefObserver = new PrefObserver("devtools."); this.#prefObserver.on( "devtools.defaultColorUnit", this.#handlePrefChange,
opts
);
// The PageStyle front related to the currently selected element this.viewedElementPageStyle = null;
this.createStyleViews();
// Add the tooltips and highlightersoverlay this.tooltips = new TooltipsOverlay(this);
}
/** * Lookup a l10n string in the shared styleinspector string bundle. * * @param {String} name * The key to lookup. * @returns {String} localized version of the given key.
*/ static l10n(name) { try { return STYLE_INSPECTOR_L10N.getStr(name);
} catch (ex) {
console.log("Error reading '" + name + "'"); thrownew Error("l10n error with " + name);
}
}
#abortController;
#contextMenu;
#computed;
#createViewsProcess;
#createViewsPromise; // Used for cancelling timeouts in the style filter.
#filterChangedTimeout = null;
#highlighters;
#isDestroyed = false; // Cache the list of properties that match the selected element.
#matchedProperties = null;
#outputParser = null;
#prefObserver;
#refreshProcess;
#sourceFilter; // The element that we're inspecting, and the document that it comes from.
#viewedElement = null;
// Number of visible properties
numVisibleProperties = 0;
get outputParser() { returnthis.#outputParser;
}
get computed() { returnthis.#computed;
}
get contextMenu() { if (!this.#contextMenu) { this.#contextMenu = new StyleInspectorMenu(this);
}
returnthis.#contextMenu;
}
// Get the highlighters overlay from the Inspector.
get highlighters() { if (!this.#highlighters) { // highlighters is a lazy getter in the inspector. this.#highlighters = this.inspector.highlighters;
}
returnthis.#highlighters;
}
get includeBrowserStyles() { returnthis.includeBrowserStylesCheckbox.checked;
}
get ruleView() { return ( this.inspector.hasPanel("ruleview") && this.inspector.getPanel("ruleview").view
);
}
get viewedElement() { returnthis.#viewedElement;
}
/** * Update the view with a new selected element. The CssComputedView panel * will show the style information for the given element. * * @param {NodeFront} element * The highlighted node to get styles for. * @returns a promise that will be resolved when highlighting is complete.
*/
selectElement(element) { if (!element) { if (this.viewedElementPageStyle) { this.viewedElementPageStyle.off( "stylesheet-updated", this.refreshPanel
); this.viewedElementPageStyle = null;
} this.#viewedElement = null; this.noResults.hidden = false;
if (this.#refreshProcess) { this.#refreshProcess.cancel();
} // Hiding all properties for (const propView of this.propertyViews) {
propView.refresh();
} return Promise.resolve(undefined);
}
if (element === this.#viewedElement) { return Promise.resolve(undefined);
}
/** * Get the type of a given node in the computed-view * * @param {DOMNode} node * The node which we want information about * @return {Object} The type information object contains the following props: * - view {String} Always "computed" to indicate the computed view. * - type {String} One of the VIEW_NODE_XXX_TYPE const in * client/inspector/shared/node-types * - value {Object} Depends on the type of the node * returns null if the node isn't anything we care about
*/ // eslint-disable-next-line complexity
getNodeInfo(node) { if (!node) { returnnull;
}
const classes = node.classList;
// Check if the node isn't a selector first since this doesn't require // walking the DOM if (
classes.contains("matched") ||
classes.contains("bestmatch") ||
classes.contains("parentmatch")
) {
let selectorText = "";
for (const child of node.childNodes[1].childNodes) { if (child.nodeType === node.TEXT_NODE) {
selectorText += child.textContent;
}
} return {
type: VIEW_NODE_SELECTOR_TYPE,
value: selectorText.trim(),
};
}
// Get the property and value for a node that's a property name or value const isHref =
classes.contains("theme-link") && !classes.contains("computed-link");
/** * Refresh the panel content. This could be called by a "ruleview-changed" event, but * we avoid the extra processing unless the panel is visible.
*/
async refreshPanel() { if (!this.#viewedElement || !this.isPanelVisible()) { return;
}
// Capture the current viewed element to return from the promise handler // early if it changed const viewedElement = this.#viewedElement;
try { // Create the properties views only once for the whole lifecycle of the inspector // via `_createPropertyViews`. // The properties are created without backend data. This queries typical property // names via `DOMWindow.getComputedStyle` on the frontend inspector document. // We then have to manually update the list of PropertyView's for custom properties // based on backend data (`getComputed()`/`computed`). // Also note that PropertyView/PropertyView are refreshed via their refresh method // which will ultimately query `CssComputedView._computed`, which we update in this method. const [computed] = await Promise.all([ this.viewedElementPageStyle.getComputed(this.#viewedElement, {
filter: this.#sourceFilter,
onlyMatched: !this.includeBrowserStyles,
markMatched: true,
}), this.#createPropertyViews(),
]);
if (viewedElement !== this.#viewedElement) { return;
}
this.#computed = computed; this.#matchedProperties = new Set(); const customProperties = new Set();
for (const name in computed) { if (computed[name].matched) { this.#matchedProperties.add(name);
} if (name.startsWith("--")) {
customProperties.add(name);
}
}
// Removing custom property PropertyViews which won't be used
let customPropertiesStartIndex; for (let i = this.propertyViews.length - 1; i >= 0; i--) { const propView = this.propertyViews[i];
// custom properties are displayed at the bottom of the list, and we're looping // backward through propertyViews, so if the current item does not represent // a custom property, we can stop looping. if (!propView.isCustomProperty) {
customPropertiesStartIndex = i + 1; break;
}
// If the custom property will be used, move to the next item. if (customProperties.has(propView.name)) {
customProperties.delete(propView.name); continue;
}
// Otherwise remove property view element if (propView.element) {
propView.element.remove();
}
// At this point, `customProperties` only contains custom property names for // which we don't have a PropertyView yet.
let insertIndex = customPropertiesStartIndex; for (const customPropertyName of Array.from(customProperties).sort()) { const propertyView = new PropertyView( this,
customPropertyName, // isCustomProperty true
);
const len = this.propertyViews.length; if (insertIndex !== len) { for (let i = insertIndex; i <= len; i++) { const existingPropView = this.propertyViews[i]; if (
!existingPropView ||
!existingPropView.isCustomProperty ||
customPropertyName < existingPropView.name
) {
insertIndex = i; break;
}
}
} this.propertyViews.splice(insertIndex, 0, propertyView);
// Insert the custom property PropertyView at the right spot so we // keep the list ordered. const previousSibling = this.element.childNodes[insertIndex - 1];
previousSibling.insertAdjacentElement( "afterend",
propertyView.createListItemElement()
);
}
if (this.#refreshProcess) { this.#refreshProcess.cancel();
}
/** * Handle the shortcut events in the computed view.
*/
#onShortcut = (name, event) => { if (!event.target.closest("#sidebar-panel-computedview")) { return;
} // Handle the search box's keypress event. If the escape key is pressed, // clear the search box field. if (
name === "Escape" &&
event.target === this.searchField && this.#onClearSearch()
) {
event.preventDefault();
event.stopPropagation();
} elseif (name === "CmdOrCtrl+F") { this.searchField.focus();
event.preventDefault();
}
};
/** * Set the filter style search value. * @param {String} value * The search value.
*/
setFilterStyles(value = "") { this.searchField.value = value; this.searchField.focus(); this.#onFilterStyles();
}
/** * Called when the user enters a search term in the filter style search box.
*/
#onFilterStyles = () => { if (this.#filterChangedTimeout) {
clearTimeout(this.#filterChangedTimeout);
}
/** * Called when the user clicks on the clear button in the filter style search * box. Returns true if the search box is cleared and false otherwise.
*/
#onClearSearch = () => { if (this.searchField.value) { this.setFilterStyles(""); returntrue;
}
returnfalse;
};
/** * The change event handler for the includeBrowserStyles checkbox.
*/
#onIncludeBrowserStyles = () => { this.refreshSourceFilter(); this.refreshPanel();
};
/** * When includeBrowserStylesCheckbox.checked is false we only display * properties that have matched selectors and have been included by the * document or one of thedocument's stylesheets. If .checked is false we * display all properties including those that come from UA stylesheets.
*/
refreshSourceFilter() { this.#matchedProperties = null; this.#sourceFilter = this.includeBrowserStyles
? CssLogic.FILTER.UA
: CssLogic.FILTER.USER;
}
/** * The CSS as displayed by the UI.
*/
createStyleViews() { if (CssComputedView.propertyNames) { return;
}
CssComputedView.propertyNames = [];
// Here we build and cache a list of css properties supported by the browser // We could use any element but let's use the main document's root element const styles = this.styleWindow.getComputedStyle( this.styleDocument.documentElement
); const mozProps = []; for (let i = 0, numStyles = styles.length; i < numStyles; i++) { const prop = styles.item(i); if (prop.startsWith("--")) { // Skip any CSS variables used inside of browser CSS files continue;
} elseif (prop.startsWith("-")) {
mozProps.push(prop);
} else {
CssComputedView.propertyNames.push(prop);
}
}
this.#createPropertyViews().catch(e => { if (!this.#isDestroyed) {
console.warn( "The creation of property views was cancelled because " + "the computed-view was destroyed before it was done creating views"
);
} else {
console.error(e);
}
});
}
/** * Get a set of properties that have matched selectors. * * @return {Set} If a property name is in the set, it has matching selectors.
*/
get matchedProperties() { returnthis.#matchedProperties || new Set();
}
/** * Focus the window on mousedown.
*/
focusWindow() { this.styleWindow.focus();
}
/** * Context menu handler.
*/
#onContextMenu = event => { // Call stopPropagation() and preventDefault() here so that avoid to show default // context menu in about:devtools-toolbox. See Bug 1515265.
event.stopPropagation();
event.preventDefault(); this.contextMenu.show(event);
};
/** * Copy the current selection to the clipboard
*/
copySelection() { try { const win = this.styleWindow; const text = win.getSelection().toString().trim();
if (this.viewedElementPageStyle) { this.viewedElementPageStyle = null;
} this.#outputParser = null;
this.#prefObserver.destroy();
// Cancel tree construction if (this.#createViewsProcess) { this.#createViewsProcess.cancel();
} if (this.#refreshProcess) { this.#refreshProcess.cancel();
}
if (this.#contextMenu) { this.#contextMenu.destroy(); this.#contextMenu = null;
}
if (this.#highlighters) { this.#highlighters.removeFromView(this); this.#highlighters = null;
}
this.tooltips.destroy();
// Nodes used in templating this.element = null; this.searchField = null; this.searchClearButton = null; this.includeBrowserStylesCheckbox = null;
// Property views for (const propView of this.propertyViews) {
propView.destroy();
} this.propertyViews = null;
class PropertyInfo { /* * @param {CssComputedView} tree * The CssComputedView instance we are working with. * @param {String} name * The CSS property name
*/
constructor(tree, name) { this.#tree = tree; this.name = name;
}
#tree;
get isSupported() { // There can be a mismatch between the list of properties // supported on the server and on the client. // Ideally we should build PropertyInfo only for property names supported on // the server. See Bug 1722348. returnthis.#tree.computed && this.name in this.#tree.computed;
}
get value() { if (this.isSupported) { const value = this.#tree.computed[this.name].value; return value;
} returnnull;
}
}
/** * A container to give easy access to property data from the template engine.
*/ class PropertyView { /* * @param {CssComputedView} tree * The CssComputedView instance we are working with. * @param {String} name * The CSS property name for which this PropertyView * instance will render the rules. * @param {Boolean} isCustomProperty * Set to true if this will represent a custom property.
*/
constructor(tree, name, isCustomProperty = false) { this.#tree = tree; this.name = name;
// AbortController for event listeners
#abortController = null;
// Cache for matched selector views
#matchedSelectorViews = null;
// The previously selected element used for the selector view caches
#prevViewedElement = null;
// PropertyInfo
#propertyInfo = null;
#tree;
/** * Get the computed style for the current property. * * @return {String} the computed style for the current property of the * currently highlighted element.
*/
get value() { returnthis.propertyInfo.value;
}
/** * An easy way to access the CssPropertyInfo behind this PropertyView.
*/
get propertyInfo() { returnthis.#propertyInfo;
}
/** * Does the property have any matched selectors?
*/
get hasMatchedSelectors() { returnthis.#tree.matchedProperties.has(this.name);
}
/** * Should this property be visible?
*/
get visible() { if (!this.#tree.viewedElement) { returnfalse;
}
if (!this.#tree.includeBrowserStyles && !this.hasMatchedSelectors) { returnfalse;
}
/** * Returns the className that should be assigned to the propertyView. * * @return {String}
*/
get propertyHeaderClassName() { returnthis.visible ? "computed-property-view" : "computed-property-hidden";
}
/** * Is the property invalid at computed value time * * @returns {Boolean}
*/
get invalidAtComputedValueTime() { returnthis.#tree.computed[this.name].invalidAtComputedValueTime;
}
/** * If this is a registered property, returns its syntax (e.g. "<color>") * * @returns {Text|undefined}
*/
get registeredPropertySyntax() { returnthis.#tree.computed[this.name].registeredPropertySyntax;
}
/** * If this is a registered property, return its initial-value * * @returns {Text|undefined}
*/
get registeredPropertyInitialValue() { returnthis.#tree.computed[this.name].registeredPropertyInitialValue;
}
/** * Create DOM elements for a property * * @return {Element} The <li> element
*/
createListItemElement() { const doc = this.#tree.styleDocument; const baseEventListenerConfig = { signal: this.#abortController.signal };
// Build the container element this.onMatchedToggle = this.onMatchedToggle.bind(this); this.element = doc.createElement("li"); this.element.className = this.propertyHeaderClassName; this.element.addEventListener( "dblclick", this.onMatchedToggle,
baseEventListenerConfig
);
// Make it keyboard navigable this.element.setAttribute("tabindex", "0"); this.shortcuts = new KeyShortcuts({
window: this.#tree.styleWindow,
target: this.element,
}); this.shortcuts.on("F1", event => { this.mdnLinkClick(event); // Prevent opening the options panel
event.preventDefault();
event.stopPropagation();
}); this.shortcuts.on("Return", this.onMatchedToggle); this.shortcuts.on("Space", this.onMatchedToggle);
// Build the style name element const nameNode = doc.createElement("span");
nameNode.classList.add("computed-property-name", "theme-fg-color3");
// Give it a heading role for screen readers.
nameNode.setAttribute("role", "heading");
// Reset its tabindex attribute otherwise, if an ellipsis is applied // it will be reachable via TABing
nameNode.setAttribute("tabindex", ""); // Avoid english text (css properties) from being altered // by RTL mode
nameNode.setAttribute("dir", "ltr");
nameNode.textContent = nameNode.title = this.name; // Make it hand over the focus to the container const focusElement = () => this.element.focus();
nameNode.addEventListener("click", focusElement, baseEventListenerConfig);
// Build the style name ":" separator const nameSeparator = doc.createElement("span");
nameSeparator.classList.add("visually-hidden");
nameSeparator.textContent = ": ";
nameNode.appendChild(nameSeparator);
// Build the style value element this.valueNode = doc.createElement("span"); this.valueNode.classList.add("computed-property-value", "theme-fg-color1"); // Reset its tabindex attribute otherwise, if an ellipsis is applied // it will be reachable via TABing this.valueNode.setAttribute("tabindex", ""); this.valueNode.setAttribute("dir", "ltr"); // Make it hand over the focus to the container this.valueNode.addEventListener( "click",
focusElement,
baseEventListenerConfig
);
// Build the style value ";" separator const valueSeparator = doc.createElement("span");
valueSeparator.classList.add("visually-hidden");
valueSeparator.textContent = ";";
// If the value is invalid at computed value time (IACVT), display the same // warning icon that we have in the rules view for IACVT declarations. if (this.isCustomProperty) { this.invalidAtComputedValueTimeNode = doc.createElement("div"); this.invalidAtComputedValueTimeNode.classList.add( "invalid-at-computed-value-time-warning"
); this.refreshInvalidAtComputedValueTime();
valueContainer.append(this.invalidAtComputedValueTimeNode);
}
// Build the matched selectors container this.matchedSelectorsContainer = doc.createElement("div"); this.matchedSelectorsContainer.classList.add("matchedselectors");
this.valueNode.innerHTML = ""; // No need to pass the baseURI argument here as computed URIs are never relative. this.valueNode.appendChild(this.#parseValue(this.propertyInfo.value));
// Add an explicit status text span for screen readers. // They won't pick up the title from the status span.
createChild(status, "span", {
dir: "ltr", class: "visually-hidden",
textContent: selector.statusText + " ",
});
// If the value is invalid at computed value time (IACVT), display the same // warning icon that we have in the rules view for IACVT declarations. if (selector.selectorInfo.invalidAtComputedValueTime) {
createChild(status, "div", { class: "invalid-at-computed-value-time-warning",
title: STYLE_INSPECTOR_L10N.getFormatStr( "rule.warningInvalidAtComputedValueTime.title",
`"${selector.selectorInfo.registeredPropertySyntax}"`
),
});
}
}
if (this.registeredPropertyInitialValue !== undefined) { const p = createChild(frag, "p"); const status = createChild(p, "span", {
dir: "ltr", class: "rule-text theme-fg-color3",
});
/** * Parse a property value using the OutputParser. * * @param {String} value * @param {String} baseURI * @returns {DocumentFragment|Element}
*/
#parseValue(value, baseURI) { if (this.isCustomProperty && value === "") { const doc = this.#tree.styleDocument; const el = doc.createElement("span");
el.classList.add("empty-css-variable");
el.append(doc.createTextNode(`<${L10N_EMPTY_VARIABLE}>`)); return el;
}
// Sadly, because this fragment is added to the template by DOM Templater // we lose any events that are attached. This means that URLs will open in a // new window. At some point we should fix this by stopping using the // templater. returnthis.#tree.outputParser.parseCssProperty(this.name, value, {
colorSwatchClass: "inspector-swatch inspector-colorswatch",
colorSwatchReadOnly: true,
colorClass: "computed-color",
urlClass: "theme-link",
fontFamilyClass: "computed-font-family",
baseURI,
});
}
/** * Provide access to the matched SelectorViews that we are currently * displaying.
*/
get matchedSelectorViews() { if (!this.#matchedSelectorViews) { this.#matchedSelectorViews = []; this.#matchedSelectorResponse.forEach(selectorInfo => { const selectorView = new SelectorView(this.#tree, selectorInfo); this.#matchedSelectorViews.push(selectorView);
}, this);
} returnthis.#matchedSelectorViews;
}
/** * The action when a user expands matched selectors. * * @param {Event} event * Used to determine the class name of the targets click * event.
*/
onMatchedToggle(event) { if (event.shiftKey) { return;
} this.matchedExpanded = !this.matchedExpanded; this.refreshMatchedSelectors();
event.preventDefault();
}
/** * The action when a user clicks on the MDN help link for a property.
*/
mdnLinkClick() { if (!this.link) { return;
}
openContentLink(this.link);
}
/** * Destroy this property view, removing event listeners
*/
destroy() { if (this.#matchedSelectorViews) { for (const view of this.#matchedSelectorViews) {
view.destroy();
}
}
if (this.#abortController) { this.#abortController.abort(); this.#abortController = null;
}
/** * A container to give us easy access to display data from a CssRule
*/ class SelectorView { /** * @param CssComputedView tree * the owning CssComputedView * @param selectorInfo
*/
constructor(tree, selectorInfo) { this.#tree = tree; this.selectorInfo = selectorInfo; this.#cacheStatusNames();
/** * Cache localized status names. * * These statuses are localized inside the styleinspector.properties string * bundle. * @see css-logic.js - the CssLogic.STATUS array.
*/
#cacheStatusNames() { if (SelectorView.STATUS_NAMES.length) { return;
}
for (const status in CssLogic.STATUS) { const i = CssLogic.STATUS[status]; if (i > CssLogic.STATUS.UNMATCHED) { const value = CssComputedView.l10n("rule.status." + status); // Replace normal spaces with non-breaking spaces
SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0");
}
}
}
/** * A localized version of cssRule.status
*/
get statusText() { return SelectorView.STATUS_NAMES[this.selectorInfo.status];
}
/** * Get class name for selector depending on status
*/
get statusClass() { return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
}
get sourceText() { returnthis.selectorInfo.sourceText;
}
get value() { returnthis.selectorInfo.value;
}
/** * Update the text of the source link to reflect whether we're showing * original sources or not. This is a callback for * SourceMapURLService.subscribe, which see. * * @param {Object | null} originalLocation * The original position object (url/line/column) or null.
*/
#updateLocation = originalLocation => { if (!this.#tree.element) { return;
}
// Update |currentLocation| to be whichever location is being // displayed at the moment.
let currentLocation = this.#generatedLocation; if (originalLocation) { const { url, line, column } = originalLocation;
currentLocation = { href: url, line, column };
}
const selector = '[sourcelocation="' + this.source + '"]'; const link = this.#tree.element.querySelector(selector); if (link) { const text =
CssLogic.shortSource(currentLocation) + ":" + currentLocation.line;
link.textContent = text;
}
/** * When a css link is clicked this method is called in order to either: * 1. Open the link in view source (for chrome stylesheets). * 2. Open the link in the style editor. * * We can only view stylesheets contained in document.styleSheets inside the * style editor.
*/
openStyleEditor() { const inspector = this.#tree.inspector; const rule = this.selectorInfo.rule;
// The style editor can only display stylesheets coming from content because // chrome stylesheets are not listed in the editor's stylesheet selector. // // If the stylesheet is a content stylesheet we send it to the style // editor else we display it in the view source window. const parentStyleSheet = rule.parentStyleSheet; if (!parentStyleSheet || parentStyleSheet.isSystem) {
inspector.toolbox.viewSource(rule.href, rule.line); return;
}
isPanelVisible() { if (!this.computedView) { returnfalse;
} returnthis.computedView.isPanelVisible();
}
onDetachedFront() { this.onSelected(false);
}
async onSelected(selectElement = true) { // Ignore the event if the view has been destroyed, or if it's inactive. // But only if the current selection isn't null. If it's been set to null, // let the update go through as this is needed to empty the view on // navigation. if (!this.computedView) { return;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.