/* 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;