summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/changes/ChangesView.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/inspector/changes/ChangesView.js')
-rw-r--r--devtools/client/inspector/changes/ChangesView.js284
1 files changed, 284 insertions, 0 deletions
diff --git a/devtools/client/inspector/changes/ChangesView.js b/devtools/client/inspector/changes/ChangesView.js
new file mode 100644
index 0000000000..20c70137a7
--- /dev/null
+++ b/devtools/client/inspector/changes/ChangesView.js
@@ -0,0 +1,284 @@
+/* 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");
+
+loader.lazyRequireGetter(
+ this,
+ "ChangesContextMenu",
+ "resource://devtools/client/inspector/changes/ChangesContextMenu.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "clipboardHelper",
+ "resource://devtools/shared/platform/clipboard.js"
+);
+
+const changesReducer = require("resource://devtools/client/inspector/changes/reducers/changes.js");
+const {
+ getChangesStylesheet,
+} = require("resource://devtools/client/inspector/changes/selectors/changes.js");
+const {
+ resetChanges,
+ trackChange,
+} = require("resource://devtools/client/inspector/changes/actions/changes.js");
+
+const ChangesApp = createFactory(
+ require("resource://devtools/client/inspector/changes/components/ChangesApp.js")
+);
+
+class ChangesView {
+ constructor(inspector, window) {
+ this.document = window.document;
+ this.inspector = inspector;
+ this.store = this.inspector.store;
+ this.telemetry = this.inspector.telemetry;
+ this.window = window;
+
+ this.store.injectReducer("changes", changesReducer);
+
+ this.onAddChange = this.onAddChange.bind(this);
+ this.onContextMenu = this.onContextMenu.bind(this);
+ this.onCopy = this.onCopy.bind(this);
+ this.onCopyAllChanges = this.copyAllChanges.bind(this);
+ this.onCopyDeclaration = this.copyDeclaration.bind(this);
+ this.onCopyRule = this.copyRule.bind(this);
+ this.onClearChanges = this.onClearChanges.bind(this);
+ this.onSelectAll = this.onSelectAll.bind(this);
+ this.onResourceAvailable = this.onResourceAvailable.bind(this);
+
+ this.destroy = this.destroy.bind(this);
+
+ this.init();
+ }
+
+ get contextMenu() {
+ if (!this._contextMenu) {
+ this._contextMenu = new ChangesContextMenu({
+ onCopy: this.onCopy,
+ onCopyAllChanges: this.onCopyAllChanges,
+ onCopyDeclaration: this.onCopyDeclaration,
+ onCopyRule: this.onCopyRule,
+ onSelectAll: this.onSelectAll,
+ toolboxDocument: this.inspector.toolbox.doc,
+ window: this.window,
+ });
+ }
+
+ return this._contextMenu;
+ }
+
+ get resourceCommand() {
+ return this.inspector.toolbox.resourceCommand;
+ }
+
+ init() {
+ const changesApp = ChangesApp({
+ onContextMenu: this.onContextMenu,
+ onCopyAllChanges: this.onCopyAllChanges,
+ onCopyRule: this.onCopyRule,
+ });
+
+ // Expose the provider to let inspector.js use it in setupSidebar.
+ this.provider = createElement(
+ Provider,
+ {
+ id: "changesview",
+ key: "changesview",
+ store: this.store,
+ },
+ changesApp
+ );
+
+ this.watchResources();
+ }
+
+ async watchResources() {
+ await this.resourceCommand.watchResources(
+ [this.resourceCommand.TYPES.DOCUMENT_EVENT],
+ {
+ onAvailable: this.onResourceAvailable,
+ // Ignore any DOCUMENT_EVENT resources that have occured in the past
+ // and are cached by the resource command, otherwise the Changes panel will
+ // react to them erroneously and interpret that the document is reloading *now*
+ // which leads to clearing all stored changes.
+ ignoreExistingResources: true,
+ }
+ );
+
+ await this.resourceCommand.watchResources(
+ [this.resourceCommand.TYPES.CSS_CHANGE],
+ { onAvailable: this.onResourceAvailable }
+ );
+ }
+
+ onResourceAvailable(resources) {
+ for (const resource of resources) {
+ if (resource.resourceType === this.resourceCommand.TYPES.CSS_CHANGE) {
+ this.onAddChange(resource);
+ continue;
+ }
+
+ if (resource.name === "dom-loading" && resource.targetFront.isTopLevel) {
+ // will-navigate doesn't work when we navigate to a new process,
+ // and for now, onTargetAvailable/onTargetDestroyed doesn't fire on navigation and
+ // only when navigating to another process.
+ // So we fallback on DOCUMENT_EVENTS to be notified when we navigate. When we
+ // navigate within the same process as well as when we navigate to a new process.
+ // (We would probably revisit that in bug 1632141)
+ this.onClearChanges();
+ }
+ }
+ }
+
+ /**
+ * Handler for the "Copy All Changes" button. Simple wrapper that just calls
+ * |this.copyChanges()| with no filters in order to trigger default operation.
+ */
+ copyAllChanges() {
+ this.copyChanges();
+ }
+
+ /**
+ * Handler for the "Copy Changes" option from the context menu.
+ * Builds a CSS text with the aggregated changes and copies it to the clipboard.
+ *
+ * Optional rule and source ids can be used to filter the scope of the operation:
+ * - if both a rule id and source id are provided, copy only the changes to the
+ * matching rule within the matching source.
+ * - if only a source id is provided, copy the changes to all rules within the
+ * matching source.
+ * - if neither rule id nor source id are provided, copy the changes too all rules
+ * within all sources.
+ *
+ * @param {String|null} ruleId
+ * Optional rule id.
+ * @param {String|null} sourceId
+ * Optional source id.
+ */
+ copyChanges(ruleId, sourceId) {
+ const state = this.store.getState().changes || {};
+ const filter = {};
+ if (ruleId) {
+ filter.ruleIds = [ruleId];
+ }
+ if (sourceId) {
+ filter.sourceIds = [sourceId];
+ }
+
+ const text = getChangesStylesheet(state, filter);
+ clipboardHelper.copyString(text);
+ }
+
+ /**
+ * Handler for the "Copy Declaration" option from the context menu.
+ * Builds a CSS declaration string with the property name and value, and copies it
+ * to the clipboard. The declaration is commented out if it is marked as removed.
+ *
+ * @param {DOMElement} element
+ * Host element of a CSS declaration rendered the Changes panel.
+ */
+ copyDeclaration(element) {
+ const name = element.querySelector(
+ ".changes__declaration-name"
+ ).textContent;
+ const value = element.querySelector(
+ ".changes__declaration-value"
+ ).textContent;
+ const isRemoved = element.classList.contains("diff-remove");
+ const text = isRemoved ? `/* ${name}: ${value}; */` : `${name}: ${value};`;
+ clipboardHelper.copyString(text);
+ }
+
+ /**
+ * Handler for the "Copy Rule" option from the context menu and "Copy Rule" button.
+ * Gets the full content of the target CSS rule (including any changes applied)
+ * and copies it to the clipboard.
+ *
+ * @param {String} ruleId
+ * Rule id of the target CSS rule.
+ */
+ async copyRule(ruleId) {
+ const inspectorFronts = await this.inspector.getAllInspectorFronts();
+
+ for (const inspectorFront of inspectorFronts) {
+ const rule = await inspectorFront.pageStyle.getRule(ruleId);
+
+ if (rule) {
+ const text = await rule.getRuleText();
+ clipboardHelper.copyString(text);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handler for the "Copy" option from the context menu.
+ * Copies the current text selection to the clipboard.
+ */
+ onCopy() {
+ clipboardHelper.copyString(this.window.getSelection().toString());
+ }
+
+ onAddChange(change) {
+ // Turn data into a suitable change to send to the store.
+ this.store.dispatch(trackChange(change));
+ }
+
+ onClearChanges() {
+ this.store.dispatch(resetChanges());
+ }
+
+ /**
+ * Select all text.
+ */
+ onSelectAll() {
+ const selection = this.window.getSelection();
+ selection.selectAllChildren(
+ this.document.getElementById("sidebar-panel-changes")
+ );
+ }
+
+ /**
+ * Event handler for the "contextmenu" event fired when the context menu is requested.
+ * @param {Event} e
+ */
+ onContextMenu(e) {
+ this.contextMenu.show(e);
+ }
+
+ /**
+ * Destruction function called when the inspector is destroyed.
+ */
+ destroy() {
+ this.resourceCommand.unwatchResources(
+ [
+ this.resourceCommand.TYPES.CSS_CHANGE,
+ this.resourceCommand.TYPES.DOCUMENT_EVENT,
+ ],
+ { onAvailable: this.onResourceAvailable }
+ );
+
+ this.store.dispatch(resetChanges());
+
+ this.document = null;
+ this.inspector = null;
+ this.store = null;
+
+ if (this._contextMenu) {
+ this._contextMenu.destroy();
+ this._contextMenu = null;
+ }
+ }
+}
+
+module.exports = ChangesView;