diff options
Diffstat (limited to 'devtools/client/inspector/changes/ChangesView.js')
-rw-r--r-- | devtools/client/inspector/changes/ChangesView.js | 284 |
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; |