Files
tubestation/devtools/client/inspector/changes/ChangesView.js
2019-07-05 11:24:38 +02:00

266 lines
8.1 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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("devtools/client/shared/vendor/react");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
loader.lazyRequireGetter(
this,
"ChangesContextMenu",
"devtools/client/inspector/changes/ChangesContextMenu"
);
loader.lazyRequireGetter(
this,
"clipboardHelper",
"devtools/shared/platform/clipboard"
);
const ChangesApp = createFactory(require("./components/ChangesApp"));
const { getChangesStylesheet } = require("./selectors/changes");
const {
TELEMETRY_SCALAR_CONTEXTMENU,
TELEMETRY_SCALAR_CONTEXTMENU_COPY,
TELEMETRY_SCALAR_CONTEXTMENU_COPY_DECLARATION,
TELEMETRY_SCALAR_CONTEXTMENU_COPY_RULE,
TELEMETRY_SCALAR_COPY,
TELEMETRY_SCALAR_COPY_ALL_CHANGES,
TELEMETRY_SCALAR_COPY_RULE,
} = require("./constants");
const { resetChanges, trackChange } = require("./actions/changes");
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.onAddChange = this.onAddChange.bind(this);
this.onClearChanges = this.onClearChanges.bind(this);
this.onChangesFront = this.onChangesFront.bind(this);
this.onContextMenu = this.onContextMenu.bind(this);
this.onCopy = this.onCopy.bind(this);
this.onCopyAllChanges = this.copyAllChanges.bind(this);
this.onCopyRule = this.copyRule.bind(this);
this.destroy = this.destroy.bind(this);
this.init();
}
get contextMenu() {
if (!this._contextMenu) {
this._contextMenu = new ChangesContextMenu(this);
}
return this._contextMenu;
}
init() {
const changesApp = ChangesApp({
onContextMenu: this.onContextMenu,
onCopy: this.onCopy,
onCopyAllChanges: this.onCopyAllChanges,
onCopyRule: this.onCopyRule,
});
// listen to the front for initialization, add listeners
// when it is ready
this._getChangesFront();
// Expose the provider to let inspector.js use it in setupSidebar.
this.provider = createElement(
Provider,
{
id: "changesview",
key: "changesview",
store: this.store,
},
changesApp
);
this.inspector.target.on("will-navigate", this.onClearChanges);
}
_getChangesFront() {
if (this.changesFrontPromise) {
return this.changesFrontPromise;
}
this.changesFrontPromise = new Promise(async resolve => {
const target = this.inspector.target;
const front = await target.getFront("changes");
this.onChangesFront(front);
resolve(front);
});
return this.changesFrontPromise;
}
async onChangesFront(changesFront) {
changesFront.on("add-change", this.onAddChange);
changesFront.on("clear-changes", this.onClearChanges);
try {
// Get all changes collected up to this point by the ChangesActor on the server,
// then push them to the Redux store here on the client.
const changes = await changesFront.allChanges();
changes.forEach(change => {
this.onAddChange(change);
});
} catch (e) {
// The connection to the server may have been cut, for
// example during test
// teardown. Here we just catch the error and silently
// ignore it.
}
}
/**
* 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();
this.telemetry.scalarAdd(TELEMETRY_SCALAR_COPY_ALL_CHANGES, 1);
}
/**
* 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);
this.telemetry.scalarAdd(TELEMETRY_SCALAR_CONTEXTMENU_COPY_DECLARATION, 1);
}
/**
* 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.
* @param {Boolean} usingContextMenu
* True if the handler is invoked from the context menu.
* (Default) False if invoked from the button.
*/
async copyRule(ruleId, usingContextMenu = false) {
const rule = await this.inspector.pageStyle.getRule(ruleId);
const text = await rule.getRuleText();
clipboardHelper.copyString(text);
if (usingContextMenu) {
this.telemetry.scalarAdd(TELEMETRY_SCALAR_CONTEXTMENU_COPY_RULE, 1);
} else {
this.telemetry.scalarAdd(TELEMETRY_SCALAR_COPY_RULE, 1);
}
}
/**
* Handler for the "Copy" option from the context menu.
* Copies the current text selection to the clipboard.
*/
copySelection() {
clipboardHelper.copyString(this.window.getSelection().toString());
this.telemetry.scalarAdd(TELEMETRY_SCALAR_CONTEXTMENU_COPY, 1);
}
onAddChange(change) {
// Turn data into a suitable change to send to the store.
this.store.dispatch(trackChange(change));
}
onClearChanges() {
this.store.dispatch(resetChanges());
}
/**
* Event handler for the "contextmenu" event fired when the context menu is requested.
* @param {Event} e
*/
onContextMenu(e) {
this.contextMenu.show(e);
this.telemetry.scalarAdd(TELEMETRY_SCALAR_CONTEXTMENU, 1);
}
/**
* Event handler for the "copy" event fired when content is copied to the clipboard.
* We don't change the default behavior. We only log the increment count of this action.
*/
onCopy() {
this.telemetry.scalarAdd(TELEMETRY_SCALAR_COPY, 1);
}
/**
* Destruction function called when the inspector is destroyed.
*/
async destroy() {
this.store.dispatch(resetChanges());
// ensure we finish waiting for the front before destroying.
const changesFront = await this.changesFrontPromise;
changesFront.off("add-change", this.onAddChange);
changesFront.off("clear-changes", this.onClearChanges);
this.document = null;
this.inspector = null;
this.store = null;
if (this._contextMenu) {
this._contextMenu.destroy();
this._contextMenu = null;
}
}
}
module.exports = ChangesView;