286 lines
8.6 KiB
JavaScript
286 lines
8.6 KiB
JavaScript
/* 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.mjs");
|
|
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) {
|
|
this.inspector = inspector;
|
|
|
|
this.inspector.store.injectReducer("compatibility", compatibilityReducer);
|
|
|
|
this.#init();
|
|
}
|
|
|
|
#isChangeAddedWhileHidden;
|
|
#previousChangedSelector;
|
|
#updateNodesTimeoutId;
|
|
|
|
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.commands.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 = () => {
|
|
// 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 we might get CSS_CHANGE resources 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 => {
|
|
// Since the mutations are throttled (in WalkerActor#getMutations), we might get the
|
|
// same nodeFront multiple times.
|
|
// Put them in a Set so we don't call updateNode more than once for a given front.
|
|
const targetsWithAttributeMutation = new Set();
|
|
const childListMutation = [];
|
|
|
|
for (const mutation of mutations) {
|
|
if (
|
|
mutation.type === "attributes" &&
|
|
(mutation.attributeName === "style" ||
|
|
mutation.attributeName === "class")
|
|
) {
|
|
targetsWithAttributeMutation.add(mutation.target);
|
|
}
|
|
if (mutation.type === "childList") {
|
|
childListMutation.push(mutation);
|
|
}
|
|
}
|
|
|
|
if (
|
|
targetsWithAttributeMutation.size === 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 targetsWithAttributeMutation) {
|
|
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;
|