summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/computed/computed.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/inspector/computed/computed.js
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/inspector/computed/computed.js')
-rw-r--r--devtools/client/inspector/computed/computed.js1713
1 files changed, 1713 insertions, 0 deletions
diff --git a/devtools/client/inspector/computed/computed.js b/devtools/client/inspector/computed/computed.js
new file mode 100644
index 0000000000..7d2c129c7b
--- /dev/null
+++ b/devtools/client/inspector/computed/computed.js
@@ -0,0 +1,1713 @@
+/* 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/. */
+
+"use strict";
+
+const flags = require("resource://devtools/shared/flags.js");
+const ToolDefinitions =
+ require("resource://devtools/client/definitions.js").Tools;
+const CssLogic = require("resource://devtools/shared/inspector/css-logic.js");
+const {
+ style: { ELEMENT_STYLE },
+} = require("resource://devtools/shared/constants.js");
+const OutputParser = require("resource://devtools/client/shared/output-parser.js");
+const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
+const {
+ createChild,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+const {
+ VIEW_NODE_SELECTOR_TYPE,
+ VIEW_NODE_PROPERTY_TYPE,
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_IMAGE_URL_TYPE,
+ VIEW_NODE_FONT_TYPE,
+} = require("resource://devtools/client/inspector/shared/node-types.js");
+const TooltipsOverlay = require("resource://devtools/client/inspector/shared/tooltips-overlay.js");
+
+loader.lazyRequireGetter(
+ this,
+ "StyleInspectorMenu",
+ "resource://devtools/client/inspector/shared/style-inspector-menu.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "KeyShortcuts",
+ "resource://devtools/client/shared/key-shortcuts.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "clipboardHelper",
+ "resource://devtools/shared/platform/clipboard.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+
+const STYLE_INSPECTOR_PROPERTIES =
+ "devtools/shared/locales/styleinspector.properties";
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+const L10N_TWISTY_EXPAND_LABEL = STYLE_INSPECTOR_L10N.getStr(
+ "rule.twistyExpand.label"
+);
+const L10N_TWISTY_COLLAPSE_LABEL = STYLE_INSPECTOR_L10N.getStr(
+ "rule.twistyCollapse.label"
+);
+
+const FILTER_CHANGED_TIMEOUT = 150;
+
+/**
+ * Helper for long-running processes that should yield occasionally to
+ * the mainloop.
+ *
+ * @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.
+ */
+function UpdateProcess(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;
+
+ this.canceled = false;
+}
+
+UpdateProcess.prototype = {
+ /**
+ * Error thrown when the array of items to process is empty.
+ */
+ ERROR_ITERATION_DONE: new Error("UpdateProcess iteration done"),
+
+ /**
+ * Schedule a new batch on the main loop.
+ */
+ schedule() {
+ if (this.canceled) {
+ return;
+ }
+ this._timeout = setTimeout(this._timeoutHandler.bind(this), 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 = 0;
+ }
+ this.canceled = true;
+ this.onCancel();
+ },
+
+ _timeoutHandler() {
+ this._timeout = null;
+ try {
+ this._runBatch();
+ this.schedule();
+ } catch (e) {
+ if (e === this.ERROR_ITERATION_DONE) {
+ this.onBatch();
+ this.onDone();
+ return;
+ }
+ console.error(e);
+ throw e;
+ }
+ },
+
+ _runBatch() {
+ const time = Date.now();
+ while (!this.canceled) {
+ const next = this._next();
+ this.onItem(next);
+ if (Date.now() - time > this.threshold) {
+ this.onBatch();
+ return;
+ }
+ }
+ },
+
+ /**
+ * Returns the item at the current index and increases the index.
+ * If all items have already been processed, will throw ERROR_ITERATION_DONE.
+ */
+ _next() {
+ if (this.index < this.array.length) {
+ return this.array[this.index++];
+ }
+ throw this.ERROR_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).
+ *
+ * @param {Inspector} inspector
+ * Inspector toolbox panel
+ * @param {Document} document
+ * The document that will contain the computed view.
+ */
+function CssComputedView(inspector, document) {
+ this.inspector = inspector;
+ this.styleDocument = document;
+ this.styleWindow = this.styleDocument.defaultView;
+
+ this.propertyViews = [];
+
+ this._outputParser = new OutputParser(document, inspector.cssProperties);
+
+ // Create bound methods.
+ this.focusWindow = this.focusWindow.bind(this);
+ this._onClearSearch = this._onClearSearch.bind(this);
+ this._onClick = this._onClick.bind(this);
+ this._onContextMenu = this._onContextMenu.bind(this);
+ this._onCopy = this._onCopy.bind(this);
+ this._onFilterStyles = this._onFilterStyles.bind(this);
+ this._onIncludeBrowserStyles = this._onIncludeBrowserStyles.bind(this);
+ this.refreshPanel = this.refreshPanel.bind(this);
+
+ const doc = this.styleDocument;
+ this.element = doc.getElementById("computed-property-container");
+ this.searchField = doc.getElementById("computed-searchbox");
+ this.searchClearButton = doc.getElementById("computed-searchinput-clear");
+ this.includeBrowserStylesCheckbox = doc.getElementById(
+ "browser-style-checkbox"
+ );
+
+ this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
+ this._onShortcut = this._onShortcut.bind(this);
+ this.shortcuts.on("CmdOrCtrl+F", event =>
+ this._onShortcut("CmdOrCtrl+F", event)
+ );
+ this.shortcuts.on("Escape", event => this._onShortcut("Escape", event));
+ this.styleDocument.addEventListener("copy", this._onCopy);
+ this.styleDocument.addEventListener("mousedown", this.focusWindow);
+ this.element.addEventListener("click", this._onClick);
+ this.element.addEventListener("contextmenu", this._onContextMenu);
+ this.searchField.addEventListener("input", this._onFilterStyles);
+ this.searchClearButton.addEventListener("click", this._onClearSearch);
+ this.includeBrowserStylesCheckbox.addEventListener(
+ "input",
+ this._onIncludeBrowserStyles
+ );
+
+ 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 }
+ );
+ }
+
+ 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.on(
+ "ruleview-added",
+ () => {
+ this.ruleView.on("ruleview-changed", this.refreshPanel);
+ },
+ { once: true }
+ );
+ }
+
+ if (this.ruleView) {
+ this.ruleView.on("ruleview-changed", this.refreshPanel);
+ }
+
+ 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._handlePrefChange = this._handlePrefChange.bind(this);
+ this._prefObserver = new PrefObserver("devtools.");
+ this._prefObserver.on("devtools.defaultColorUnit", this._handlePrefChange);
+
+ // The element that we're inspecting, and the document that it comes from.
+ this._viewedElement = null;
+ // 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.
+ */
+CssComputedView.l10n = function (name) {
+ try {
+ return STYLE_INSPECTOR_L10N.getStr(name);
+ } catch (ex) {
+ console.log("Error reading '" + name + "'");
+ throw new Error("l10n error with " + name);
+ }
+};
+
+CssComputedView.prototype = {
+ // Cache the list of properties that match the selected element.
+ _matchedProperties: null,
+
+ // Used for cancelling timeouts in the style filter.
+ _filterChangedTimeout: null,
+
+ // Holds the ID of the panelRefresh timeout.
+ _panelRefreshTimeout: null,
+
+ // Number of visible properties
+ numVisibleProperties: 0,
+
+ get contextMenu() {
+ if (!this._contextMenu) {
+ this._contextMenu = new StyleInspectorMenu(this);
+ }
+
+ return this._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;
+ }
+
+ return this._highlighters;
+ },
+
+ get includeBrowserStyles() {
+ return this.includeBrowserStylesCheckbox.checked;
+ },
+
+ get ruleView() {
+ return (
+ this.inspector.hasPanel("ruleview") &&
+ this.inspector.getPanel("ruleview").view
+ );
+ },
+
+ _handlePrefChange() {
+ if (this._computed) {
+ this.refreshPanel();
+ }
+ },
+
+ /**
+ * 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);
+ }
+
+ if (this.viewedElementPageStyle) {
+ this.viewedElementPageStyle.off("stylesheet-updated", this.refreshPanel);
+ }
+ this.viewedElementPageStyle = element.inspectorFront.pageStyle;
+ this.viewedElementPageStyle.on("stylesheet-updated", this.refreshPanel);
+
+ this._viewedElement = element;
+
+ this.refreshSourceFilter();
+
+ return this.refreshPanel();
+ },
+
+ /**
+ * 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) {
+ return null;
+ }
+
+ 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(),
+ };
+ }
+
+ const propertyView = node.closest(".computed-property-view");
+ const propertyMatchedSelectors = node.closest(".matchedselectors");
+ const parent = propertyMatchedSelectors || propertyView;
+
+ if (!parent) {
+ return null;
+ }
+
+ let value, type;
+
+ // 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");
+
+ if (classes.contains("computed-font-family")) {
+ if (propertyMatchedSelectors) {
+ const view = propertyMatchedSelectors.closest("li");
+ value = {
+ property: view.querySelector(".computed-property-name").firstChild
+ .textContent,
+ value: node.parentNode.textContent,
+ };
+ } else if (propertyView) {
+ value = {
+ property: parent.querySelector(".computed-property-name").firstChild
+ .textContent,
+ value: node.parentNode.textContent,
+ };
+ } else {
+ return null;
+ }
+ } else if (
+ propertyMatchedSelectors &&
+ (classes.contains("computed-other-property-value") || isHref)
+ ) {
+ const view = propertyMatchedSelectors.closest("li");
+ value = {
+ property: view.querySelector(".computed-property-name").firstChild
+ .textContent,
+ value: node.textContent,
+ };
+ } else if (
+ propertyView &&
+ (classes.contains("computed-property-name") ||
+ classes.contains("computed-property-value") ||
+ isHref)
+ ) {
+ value = {
+ property: parent.querySelector(".computed-property-name").firstChild
+ .textContent,
+ value: parent.querySelector(".computed-property-value").textContent,
+ };
+ }
+
+ // Get the type
+ if (classes.contains("computed-property-name")) {
+ type = VIEW_NODE_PROPERTY_TYPE;
+ } else if (
+ classes.contains("computed-property-value") ||
+ classes.contains("computed-other-property-value")
+ ) {
+ type = VIEW_NODE_VALUE_TYPE;
+ } else if (classes.contains("computed-font-family")) {
+ type = VIEW_NODE_FONT_TYPE;
+ } else if (isHref) {
+ type = VIEW_NODE_IMAGE_URL_TYPE;
+ value.url = node.href;
+ } else {
+ return null;
+ }
+
+ return {
+ view: "computed",
+ type,
+ value,
+ };
+ },
+
+ _createPropertyViews() {
+ if (this._createViewsPromise) {
+ return this._createViewsPromise;
+ }
+
+ this.refreshSourceFilter();
+ this.numVisibleProperties = 0;
+ const fragment = this.styleDocument.createDocumentFragment();
+
+ this._createViewsPromise = new Promise((resolve, reject) => {
+ this._createViewsProcess = new UpdateProcess(
+ this.styleWindow,
+ CssComputedView.propertyNames,
+ {
+ onItem: propertyName => {
+ // Per-item callback.
+ const propView = new PropertyView(this, propertyName);
+ fragment.append(propView.createListItemElement());
+
+ if (propView.visible) {
+ this.numVisibleProperties++;
+ }
+ this.propertyViews.push(propView);
+ },
+ onCancel: () => {
+ reject("_createPropertyViews cancelled");
+ },
+ onDone: () => {
+ // Completed callback.
+ this.element.appendChild(fragment);
+ this.noResults.hidden = this.numVisibleProperties > 0;
+ resolve(undefined);
+ },
+ }
+ );
+ });
+
+ this._createViewsProcess.schedule();
+
+ return this._createViewsPromise;
+ },
+
+ isPanelVisible() {
+ return (
+ this.inspector.toolbox &&
+ this.inspector.sidebar &&
+ this.inspector.toolbox.currentToolId === "inspector" &&
+ this.inspector.sidebar.getCurrentTabID() == "computedview"
+ );
+ },
+
+ /**
+ * Refresh the panel content. This could be called by a "ruleview-changed" event, but
+ * we avoid the extra processing unless the panel is visible.
+ */
+ refreshPanel() {
+ if (!this._viewedElement || !this.isPanelVisible()) {
+ return Promise.resolve();
+ }
+
+ // Capture the current viewed element to return from the promise handler
+ // early if it changed
+ const viewedElement = this._viewedElement;
+
+ return Promise.all([
+ this._createPropertyViews(),
+ this.viewedElementPageStyle.getComputed(this._viewedElement, {
+ filter: this._sourceFilter,
+ onlyMatched: !this.includeBrowserStyles,
+ markMatched: true,
+ }),
+ ])
+ .then(([, computed]) => {
+ if (viewedElement !== this._viewedElement) {
+ return Promise.resolve();
+ }
+
+ 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();
+ }
+
+ propView.destroy();
+ this.propertyViews.splice(i, 1);
+ }
+
+ // 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();
+ }
+
+ this.noResults.hidden = true;
+
+ // Reset visible property count
+ this.numVisibleProperties = 0;
+
+ return new Promise((resolve, reject) => {
+ this._refreshProcess = new UpdateProcess(
+ this.styleWindow,
+ this.propertyViews,
+ {
+ onItem: propView => {
+ propView.refresh();
+ },
+ onCancel: () => {
+ reject("_refreshProcess of computed view cancelled");
+ },
+ onDone: () => {
+ this._refreshProcess = null;
+ this.noResults.hidden = this.numVisibleProperties > 0;
+
+ const searchBox = this.searchField.parentNode;
+ searchBox.classList.toggle(
+ "devtools-searchbox-no-match",
+ !!this.searchField.value.length && !this.numVisibleProperties
+ );
+
+ this.inspector.emit("computed-view-refreshed");
+ resolve(undefined);
+ },
+ }
+ );
+ this._refreshProcess.schedule();
+ });
+ })
+ .catch(console.error);
+ },
+
+ /**
+ * 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();
+ } else if (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);
+ }
+
+ const filterTimeout = this.searchField.value.length
+ ? FILTER_CHANGED_TIMEOUT
+ : 0;
+ this.searchClearButton.hidden = this.searchField.value.length === 0;
+
+ this._filterChangedTimeout = setTimeout(() => {
+ this.refreshPanel();
+ this._filterChangeTimeout = null;
+ }, filterTimeout);
+ },
+
+ /**
+ * 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("");
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * 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;
+ } else if (prop.startsWith("-")) {
+ mozProps.push(prop);
+ } else {
+ CssComputedView.propertyNames.push(prop);
+ }
+ }
+
+ CssComputedView.propertyNames.sort();
+ CssComputedView.propertyNames.push.apply(
+ CssComputedView.propertyNames,
+ mozProps.sort()
+ );
+
+ 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() {
+ return this._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);
+ },
+
+ _onClick(event) {
+ const target = event.target;
+
+ if (target.nodeName === "a") {
+ event.stopPropagation();
+ event.preventDefault();
+ openContentLink(target.href);
+ }
+ },
+
+ /**
+ * Callback for copy event. Copy selected text.
+ *
+ * @param {Event} event
+ * copy event object.
+ */
+ _onCopy(event) {
+ const win = this.styleWindow;
+ const text = win.getSelection().toString().trim();
+ if (text !== "") {
+ this.copySelection();
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * Copy the current selection to the clipboard
+ */
+ copySelection() {
+ try {
+ const win = this.styleWindow;
+ const text = win.getSelection().toString().trim();
+
+ clipboardHelper.copyString(text);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Destructor for CssComputedView.
+ */
+ destroy() {
+ this._viewedElement = null;
+ if (this.viewedElementPageStyle) {
+ this.viewedElementPageStyle.off("stylesheet-updated", this.refreshPanel);
+ this.viewedElementPageStyle = null;
+ }
+ this._outputParser = null;
+
+ this._prefObserver.off("devtools.defaultColorUnit", this._handlePrefChange);
+ 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();
+
+ // Remove bound listeners
+ this.element.removeEventListener("click", this._onClick);
+ this.element.removeEventListener("contextmenu", this._onContextMenu);
+ this.searchField.removeEventListener("input", this._onFilterStyles);
+ this.searchClearButton.removeEventListener("click", this._onClearSearch);
+ this.styleDocument.removeEventListener("copy", this._onCopy);
+ this.styleDocument.removeEventListener("mousedown", this.focusWindow);
+ this.includeBrowserStylesCheckbox.removeEventListener(
+ "input",
+ this._onIncludeBrowserStyles
+ );
+
+ if (this.ruleView) {
+ this.ruleView.off("ruleview-changed", this.refreshPanel);
+ }
+
+ // 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;
+
+ this.inspector = null;
+ this.styleDocument = null;
+ this.styleWindow = null;
+
+ this._isDestroyed = true;
+ },
+};
+
+function PropertyInfo(tree, name) {
+ this.tree = tree;
+ this.name = name;
+}
+
+PropertyInfo.prototype = {
+ 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.
+ return this.tree._computed && this.name in this.tree._computed;
+ },
+
+ get value() {
+ if (this.isSupported) {
+ const value = this.tree._computed[this.name].value;
+ return value;
+ }
+ return null;
+ },
+};
+
+/**
+ * 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;
+
+ this.isCustomProperty = isCustomProperty;
+
+ if (!this.isCustomProperty) {
+ this.link = "https://developer.mozilla.org/docs/Web/CSS/" + name;
+ }
+
+ this.#propertyInfo = new PropertyInfo(tree, name);
+ const win = this.tree.styleWindow;
+ this.#abortController = new win.AbortController();
+ }
+
+ // The parent element which contains the open attribute
+ element = null;
+
+ // Property header node
+ propertyHeader = null;
+
+ // Destination for property values
+ valueNode = null;
+
+ // Are matched rules expanded?
+ matchedExpanded = false;
+
+ // Matched selector container
+ matchedSelectorsContainer = null;
+
+ // Matched selector expando
+ matchedExpander = null;
+
+ // 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;
+
+ /**
+ * Get the computed style for the current property.
+ *
+ * @return {String} the computed style for the current property of the
+ * currently highlighted element.
+ */
+ get value() {
+ return this.propertyInfo.value;
+ }
+
+ /**
+ * An easy way to access the CssPropertyInfo behind this PropertyView.
+ */
+ get propertyInfo() {
+ return this.#propertyInfo;
+ }
+
+ /**
+ * Does the property have any matched selectors?
+ */
+ get hasMatchedSelectors() {
+ return this.tree.matchedProperties.has(this.name);
+ }
+
+ /**
+ * Should this property be visible?
+ */
+ get visible() {
+ if (!this.tree._viewedElement) {
+ return false;
+ }
+
+ if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
+ return false;
+ }
+
+ const searchTerm = this.tree.searchField.value.toLowerCase();
+ const isValidSearchTerm = !!searchTerm.trim().length;
+ if (
+ isValidSearchTerm &&
+ !this.name.toLowerCase().includes(searchTerm) &&
+ !this.value.toLowerCase().includes(searchTerm)
+ ) {
+ return false;
+ }
+
+ return this.propertyInfo.isSupported;
+ }
+
+ /**
+ * Returns the className that should be assigned to the propertyView.
+ *
+ * @return {String}
+ */
+ get propertyHeaderClassName() {
+ return this.visible ? "computed-property-view" : "computed-property-hidden";
+ }
+
+ /**
+ * 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);
+
+ const nameContainer = doc.createElement("span");
+ nameContainer.className = "computed-property-name-container";
+
+ // Build the twisty expand/collapse
+ this.matchedExpander = doc.createElement("div");
+ this.matchedExpander.className = "computed-expander theme-twisty";
+ this.matchedExpander.setAttribute("role", "button");
+ this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL);
+ this.matchedExpander.addEventListener(
+ "click",
+ this.onMatchedToggle,
+ baseEventListenerConfig
+ );
+
+ // 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);
+
+ nameContainer.appendChild(nameNode);
+
+ const valueContainer = doc.createElement("span");
+ valueContainer.className = "computed-property-value-container";
+
+ // 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 = ";";
+
+ // Build the matched selectors container
+ this.matchedSelectorsContainer = doc.createElement("div");
+ this.matchedSelectorsContainer.classList.add("matchedselectors");
+
+ valueContainer.append(this.valueNode, valueSeparator);
+ this.element.append(
+ this.matchedExpander,
+ nameContainer,
+ valueContainer,
+ this.matchedSelectorsContainer
+ );
+
+ return this.element;
+ }
+
+ /**
+ * Refresh the panel's CSS property value.
+ */
+ refresh() {
+ const className = this.propertyHeaderClassName;
+ if (this.element.className !== className) {
+ this.element.className = className;
+ }
+
+ if (this.#prevViewedElement !== this.tree._viewedElement) {
+ this.#matchedSelectorViews = null;
+ this.#prevViewedElement = this.tree._viewedElement;
+ }
+
+ if (!this.tree._viewedElement || !this.visible) {
+ this.valueNode.textContent = this.valueNode.title = "";
+ this.matchedSelectorsContainer.parentNode.hidden = true;
+ this.matchedSelectorsContainer.textContent = "";
+ this.matchedExpander.removeAttribute("open");
+ this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL);
+ return;
+ }
+
+ this.tree.numVisibleProperties++;
+
+ const outputParser = this.tree._outputParser;
+ const frag = outputParser.parseCssProperty(
+ this.propertyInfo.name,
+ this.propertyInfo.value,
+ {
+ colorSwatchClass: "computed-colorswatch",
+ colorClass: "computed-color",
+ urlClass: "theme-link",
+ fontFamilyClass: "computed-font-family",
+ // No need to use baseURI here as computed URIs are never relative.
+ }
+ );
+ this.valueNode.innerHTML = "";
+ this.valueNode.appendChild(frag);
+
+ this.refreshMatchedSelectors();
+ }
+
+ /**
+ * Refresh the panel matched rules.
+ */
+ refreshMatchedSelectors() {
+ const hasMatchedSelectors = this.hasMatchedSelectors;
+ this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
+
+ if (hasMatchedSelectors) {
+ this.matchedExpander.classList.add("computed-expandable");
+ } else {
+ this.matchedExpander.classList.remove("computed-expandable");
+ }
+
+ if (this.matchedExpanded && hasMatchedSelectors) {
+ return this.tree.viewedElementPageStyle
+ .getMatchedSelectors(this.tree._viewedElement, this.name)
+ .then(matched => {
+ if (!this.matchedExpanded) {
+ return;
+ }
+
+ this._matchedSelectorResponse = matched;
+
+ this.#buildMatchedSelectors();
+ this.matchedExpander.setAttribute("open", "");
+ this.matchedExpander.setAttribute(
+ "aria-label",
+ L10N_TWISTY_COLLAPSE_LABEL
+ );
+ this.tree.inspector.emit("computed-view-property-expanded");
+ })
+ .catch(console.error);
+ }
+
+ this.matchedSelectorsContainer.innerHTML = "";
+ this.matchedExpander.removeAttribute("open");
+ this.matchedExpander.setAttribute("aria-label", L10N_TWISTY_EXPAND_LABEL);
+ this.tree.inspector.emit("computed-view-property-collapsed");
+ return Promise.resolve(undefined);
+ }
+
+ get matchedSelectors() {
+ return this._matchedSelectorResponse;
+ }
+
+ #buildMatchedSelectors() {
+ const frag = this.element.ownerDocument.createDocumentFragment();
+
+ for (const selector of this.matchedSelectorViews) {
+ const p = createChild(frag, "p");
+ const span = createChild(p, "span", {
+ class: "rule-link",
+ });
+
+ const link = createChild(span, "a", {
+ target: "_blank",
+ class: "computed-link theme-link",
+ title: selector.longSource,
+ sourcelocation: selector.source,
+ tabindex: "0",
+ textContent: selector.source,
+ });
+ link.addEventListener("click", selector.openStyleEditor);
+ const shortcuts = new KeyShortcuts({
+ window: this.tree.styleWindow,
+ target: link,
+ });
+ shortcuts.on("Return", () => selector.openStyleEditor());
+
+ const status = createChild(p, "span", {
+ dir: "ltr",
+ class: "rule-text theme-fg-color3 " + selector.statusClass,
+ title: selector.statusText,
+ });
+
+ // 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 + " ",
+ });
+
+ createChild(status, "div", {
+ class: "fix-get-selection",
+ textContent: selector.sourceText,
+ });
+
+ const valueDiv = createChild(status, "div", {
+ class:
+ "fix-get-selection computed-other-property-value theme-fg-color1",
+ });
+ valueDiv.appendChild(selector.outputFragment);
+ }
+
+ this.matchedSelectorsContainer.innerHTML = "";
+ this.matchedSelectorsContainer.appendChild(frag);
+ }
+
+ /**
+ * 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);
+ }
+ return this.#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(event) {
+ 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;
+ }
+
+ if (this.shortcuts) {
+ this.shortcuts.destroy();
+ }
+
+ this.shortcuts = null;
+ this.element = null;
+ this.matchedExpander = null;
+ this.valueNode = null;
+ }
+}
+
+/**
+ * A container to give us easy access to display data from a CssRule
+ *
+ * @param CssComputedView tree
+ * the owning CssComputedView
+ * @param selectorInfo
+ */
+function SelectorView(tree, selectorInfo) {
+ this.tree = tree;
+ this.selectorInfo = selectorInfo;
+ this._cacheStatusNames();
+
+ this.openStyleEditor = this.openStyleEditor.bind(this);
+ this._updateLocation = this._updateLocation.bind(this);
+
+ const rule = this.selectorInfo.rule;
+ if (!rule || !rule.parentStyleSheet || rule.type == ELEMENT_STYLE) {
+ this.source = CssLogic.l10n("rule.sourceElement");
+ this.longSource = this.source;
+ } else {
+ // This always refers to the generated location.
+ const sheet = rule.parentStyleSheet;
+ const sourceSuffix = rule.line > 0 ? ":" + rule.line : "";
+ this.source = CssLogic.shortSource(sheet) + sourceSuffix;
+ this.longSource = CssLogic.longSource(sheet) + sourceSuffix;
+
+ this.generatedLocation = {
+ sheet,
+ href: sheet.href || sheet.nodeHref,
+ line: rule.line,
+ column: rule.column,
+ };
+ this.sourceMapURLService = this.tree.inspector.toolbox.sourceMapURLService;
+ this._unsubscribeCallback = this.sourceMapURLService.subscribeByID(
+ this.generatedLocation.sheet.resourceId,
+ this.generatedLocation.line,
+ this.generatedLocation.column,
+ this._updateLocation
+ );
+ }
+}
+
+/**
+ * Decode for cssInfo.rule.status
+ * @see SelectorView.prototype._cacheStatusNames
+ * @see CssLogic.STATUS
+ */
+SelectorView.STATUS_NAMES = [
+ // "Parent Match", "Matched", "Best Match"
+];
+
+SelectorView.CLASS_NAMES = ["parentmatch", "matched", "bestmatch"];
+
+SelectorView.prototype = {
+ /**
+ * 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 href() {
+ if (this._href) {
+ return this._href;
+ }
+ const sheet = this.selectorInfo.rule.parentStyleSheet;
+ this._href = sheet ? sheet.href : "#";
+ return this._href;
+ },
+
+ get sourceText() {
+ return this.selectorInfo.sourceText;
+ },
+
+ get value() {
+ return this.selectorInfo.value;
+ },
+
+ get outputFragment() {
+ // 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.
+ const outputParser = this.tree._outputParser;
+ const frag = outputParser.parseCssProperty(
+ this.selectorInfo.name,
+ this.selectorInfo.value,
+ {
+ colorSwatchClass: "computed-colorswatch",
+ colorClass: "computed-color",
+ urlClass: "theme-link",
+ fontFamilyClass: "computed-font-family",
+ baseURI: this.selectorInfo.rule.href,
+ }
+ );
+ return frag;
+ },
+
+ /**
+ * 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;
+ }
+
+ this.tree.inspector.emit("computed-view-sourcelinks-updated");
+ },
+
+ /**
+ * 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;
+ }
+
+ const { sheet, line, column } = this.generatedLocation;
+ if (ToolDefinitions.styleEditor.isToolSupported(inspector.toolbox)) {
+ inspector.toolbox.viewSourceInStyleEditorByResource(sheet, line, column);
+ }
+ },
+
+ /**
+ * Destroy this selector view, removing event listeners
+ */
+ destroy() {
+ if (this._unsubscribeCallback) {
+ this._unsubscribeCallback();
+ }
+ },
+};
+
+function ComputedViewTool(inspector, window) {
+ this.inspector = inspector;
+ this.document = window.document;
+
+ this.computedView = new CssComputedView(this.inspector, this.document);
+
+ this.onDetachedFront = this.onDetachedFront.bind(this);
+ this.onSelected = this.onSelected.bind(this);
+ this.refresh = this.refresh.bind(this);
+ this.onPanelSelected = this.onPanelSelected.bind(this);
+
+ this.inspector.selection.on("detached-front", this.onDetachedFront);
+ this.inspector.selection.on("new-node-front", this.onSelected);
+ this.inspector.selection.on("pseudoclass", this.refresh);
+ this.inspector.sidebar.on("computedview-selected", this.onPanelSelected);
+ this.inspector.styleChangeTracker.on("style-changed", this.refresh);
+
+ this.computedView.selectElement(null);
+
+ this.onSelected();
+}
+
+ComputedViewTool.prototype = {
+ isPanelVisible() {
+ if (!this.computedView) {
+ return false;
+ }
+ return this.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;
+ }
+
+ const isInactive =
+ !this.isPanelVisible() && this.inspector.selection.nodeFront;
+ if (isInactive) {
+ return;
+ }
+
+ if (
+ !this.inspector.selection.isConnected() ||
+ !this.inspector.selection.isElementNode()
+ ) {
+ this.computedView.selectElement(null);
+ return;
+ }
+
+ if (selectElement) {
+ const done = this.inspector.updating("computed-view");
+ await this.computedView.selectElement(this.inspector.selection.nodeFront);
+ done();
+ }
+ },
+
+ refresh() {
+ if (this.isPanelVisible()) {
+ this.computedView.refreshPanel();
+ }
+ },
+
+ onPanelSelected() {
+ if (
+ this.inspector.selection.nodeFront === this.computedView._viewedElement
+ ) {
+ this.refresh();
+ } else {
+ this.onSelected();
+ }
+ },
+
+ destroy() {
+ this.inspector.styleChangeTracker.off("style-changed", this.refresh);
+ this.inspector.sidebar.off("computedview-selected", this.refresh);
+ this.inspector.selection.off("pseudoclass", this.refresh);
+ this.inspector.selection.off("new-node-front", this.onSelected);
+ this.inspector.selection.off("detached-front", this.onDetachedFront);
+ this.inspector.sidebar.off("computedview-selected", this.onPanelSelected);
+
+ this.computedView.destroy();
+
+ this.computedView = this.document = this.inspector = null;
+ },
+};
+
+exports.CssComputedView = CssComputedView;
+exports.ComputedViewTool = ComputedViewTool;
+exports.PropertyView = PropertyView;