summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/compatibility/CompatibilityView.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/compatibility/CompatibilityView.js277
1 files changed, 277 insertions, 0 deletions
diff --git a/devtools/client/inspector/compatibility/CompatibilityView.js b/devtools/client/inspector/compatibility/CompatibilityView.js
new file mode 100644
index 0000000000..19c3263f00
--- /dev/null
+++ b/devtools/client/inspector/compatibility/CompatibilityView.js
@@ -0,0 +1,277 @@
+/* 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;