/* 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 { createFactory, createElement, } = require("resource://devtools/client/shared/vendor/react.js"); const { Provider, } = require("resource://devtools/client/shared/vendor/react-redux.js"); const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); const LocalizationProvider = createFactory(FluentReact.LocalizationProvider); const compatibilityReducer = require("resource://devtools/client/inspector/compatibility/reducers/compatibility.js"); const { appendNode, clearDestroyedNodes, initUserSettings, removeNode, updateNodes, updateSelectedNode, updateTopLevelTarget, updateNode, } = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js"); const CompatibilityApp = createFactory( require("resource://devtools/client/inspector/compatibility/components/CompatibilityApp.js") ); class CompatibilityView { constructor(inspector, window) { this.inspector = inspector; this.inspector.store.injectReducer("compatibility", compatibilityReducer); this._parseMarkup = this._parseMarkup.bind(this); this._onChangeAdded = this._onChangeAdded.bind(this); this._onPanelSelected = this._onPanelSelected.bind(this); this._onSelectedNodeChanged = this._onSelectedNodeChanged.bind(this); this._onTopLevelTargetChanged = this._onTopLevelTargetChanged.bind(this); this._onResourceAvailable = this._onResourceAvailable.bind(this); this._onMarkupMutation = this._onMarkupMutation.bind(this); this._init(); } destroy() { try { this.resourceCommand.unwatchResources( [this.resourceCommand.TYPES.CSS_CHANGE], { onAvailable: this._onResourceAvailable, } ); } catch (e) { // If unwatchResources is called before finishing process of watchResources, // unwatchResources throws an error during stopping listener. } this.inspector.off("new-root", this._onTopLevelTargetChanged); this.inspector.off("markupmutation", this._onMarkupMutation); this.inspector.selection.off("new-node-front", this._onSelectedNodeChanged); this.inspector.sidebar.off( "compatibilityview-selected", this._onPanelSelected ); this.inspector = null; } get resourceCommand() { return this.inspector.toolbox.resourceCommand; } async _init() { const { setSelectedNode } = this.inspector.getCommonComponentProps(); const compatibilityApp = new CompatibilityApp({ setSelectedNode, }); this.provider = createElement( Provider, { id: "compatibilityview", store: this.inspector.store, }, LocalizationProvider( { bundles: this.inspector.fluentL10n.getBundles(), parseMarkup: this._parseMarkup, }, compatibilityApp ) ); await this.inspector.store.dispatch(initUserSettings()); // awaiting for `initUserSettings` makes us miss the initial "compatibilityview-selected" // event, so we need to manually call _onPanelSelected to fetch compatibility issues // for the selected node (and the whole page). this._onPanelSelected(); this.inspector.on("new-root", this._onTopLevelTargetChanged); this.inspector.on("markupmutation", this._onMarkupMutation); this.inspector.selection.on("new-node-front", this._onSelectedNodeChanged); this.inspector.sidebar.on( "compatibilityview-selected", this._onPanelSelected ); await this.resourceCommand.watchResources( [this.resourceCommand.TYPES.CSS_CHANGE], { onAvailable: this._onResourceAvailable, // CSS changes made before opening Compatibility View are already applied to // corresponding DOM at this point, so existing resources can be ignored here. ignoreExistingResources: true, } ); this.inspector.emitForTests("compatibilityview-initialized"); } _isAvailable() { return ( this.inspector && this.inspector.sidebar && this.inspector.sidebar.getCurrentTabID() === "compatibilityview" && this.inspector.selection && this.inspector.selection.isConnected() ); } _parseMarkup(str) { // Using a BrowserLoader for the inspector is currently blocked on performance regressions, // see Bug 1471853. throw new Error( "The inspector cannot use tags in ftl strings because it does not run in a BrowserLoader" ); } _onChangeAdded({ selector }) { if (!this._isAvailable()) { // In order to update this panel if a change is added while hiding this panel. this._isChangeAddedWhileHidden = true; return; } this._isChangeAddedWhileHidden = false; // We need to debounce updating nodes since "add-change" event on changes actor is // fired for every typed character until fixing bug 1503036. if (this._previousChangedSelector === selector) { clearTimeout(this._updateNodesTimeoutId); } this._previousChangedSelector = selector; this._updateNodesTimeoutId = setTimeout(() => { // TODO: In case of keyframes changes, the selector given from changes actor is // keyframe-selector such as "from" and "100%", not selector for node. Thus, // we need to address this case. this.inspector.store.dispatch(updateNodes(selector)); }, 500); } _onMarkupMutation(mutations) { const attributeMutation = mutations.filter( mutation => mutation.type === "attributes" && (mutation.attributeName === "style" || mutation.attributeName === "class") ); const childListMutation = mutations.filter( mutation => mutation.type === "childList" ); if (attributeMutation.length === 0 && childListMutation.length === 0) { return; } if (!this._isAvailable()) { // In order to update this panel if a change is added while hiding this panel. this._isChangeAddedWhileHidden = true; return; } this._isChangeAddedWhileHidden = false; // Resource Watcher doesn't respond to programmatic inline CSS // change. This check can be removed once the following bug is resolved // https://bugzilla.mozilla.org/show_bug.cgi?id=1506160 for (const { target } of attributeMutation) { this.inspector.store.dispatch(updateNode(target)); } // Destroyed nodes can be cleaned up // once at the end if necessary let cleanupDestroyedNodes = false; for (const { removed, target } of childListMutation) { if (!removed.length) { this.inspector.store.dispatch(appendNode(target)); continue; } const retainedNodes = removed.filter(node => node && !node.isDestroyed()); cleanupDestroyedNodes = cleanupDestroyedNodes || retainedNodes.length !== removed.length; for (const retainedNode of retainedNodes) { this.inspector.store.dispatch(removeNode(retainedNode)); } } if (cleanupDestroyedNodes) { this.inspector.store.dispatch(clearDestroyedNodes()); } } _onPanelSelected() { const { selectedNode, topLevelTarget } = this.inspector.store.getState().compatibility; // Update if the selected node is changed or new change is added while the panel was hidden. if ( this.inspector.selection.nodeFront !== selectedNode || this._isChangeAddedWhileHidden ) { this._onSelectedNodeChanged(); } // Update if the top target has changed or new change is added while the panel was hidden. if ( this.inspector.toolbox.target !== topLevelTarget || this._isChangeAddedWhileHidden ) { this._onTopLevelTargetChanged(); } this._isChangeAddedWhileHidden = false; } _onSelectedNodeChanged() { if (!this._isAvailable()) { return; } this.inspector.store.dispatch( updateSelectedNode(this.inspector.selection.nodeFront) ); } _onResourceAvailable(resources) { for (const resource of resources) { // Style changes applied inline directly to // the element and its changes are monitored by // _onMarkupMutation via markupmutation events. // Hence those changes can be ignored here if (resource.source?.type !== "element") { this._onChangeAdded(resource); } } } _onTopLevelTargetChanged() { if (!this._isAvailable()) { return; } this.inspector.store.dispatch( updateTopLevelTarget(this.inspector.toolbox.target) ); } } module.exports = CompatibilityView;