Files
tubestation/devtools/client/inspector/changes/ChangesView.js
Razvan Caliman 7f88bd55ca Bug 1530341 - (Part 2) Add Copy All Changes button to Changes panel. r=gl
Depends on D21007

Adds a Copy All Changes button to the toolbar of the Changes panel.
When pressed, this builds a stylesheet out of the changes for all sources tracked (stylesheets, element styles, etc)
The output format is the same as the now defunct Bug 1524548 with the added code comment as separator between the sources.

Differential Revision: https://phabricator.services.mozilla.com/D21008
2019-02-25 22:37:02 +00:00

223 lines
6.8 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, "prettifyCSS", "devtools/shared/inspector/css-logic", true);
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_COPY,
} = 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();
}
/**
* 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 Rule" option from the context menu.
* 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 rule = await this.inspector.pageStyle.getRule(ruleId);
const text = await rule.getRuleText();
const prettyCSS = prettifyCSS(text);
clipboardHelper.copyString(prettyCSS);
}
/**
* 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;