ChangesView.js (8497B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 createFactory, 9 createElement, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const { 12 Provider, 13 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 14 15 loader.lazyRequireGetter( 16 this, 17 "ChangesContextMenu", 18 "resource://devtools/client/inspector/changes/ChangesContextMenu.js" 19 ); 20 loader.lazyRequireGetter( 21 this, 22 "clipboardHelper", 23 "resource://devtools/shared/platform/clipboard.js" 24 ); 25 26 const changesReducer = require("resource://devtools/client/inspector/changes/reducers/changes.js"); 27 const { 28 getChangesStylesheet, 29 } = require("resource://devtools/client/inspector/changes/selectors/changes.js"); 30 const { 31 resetChanges, 32 trackChange, 33 } = require("resource://devtools/client/inspector/changes/actions/changes.js"); 34 35 const ChangesApp = createFactory( 36 require("resource://devtools/client/inspector/changes/components/ChangesApp.js") 37 ); 38 39 class ChangesView { 40 constructor(inspector, window) { 41 this.document = window.document; 42 this.inspector = inspector; 43 this.store = this.inspector.store; 44 this.telemetry = this.inspector.telemetry; 45 this.window = window; 46 47 this.store.injectReducer("changes", changesReducer); 48 49 this.onAddChange = this.onAddChange.bind(this); 50 this.onContextMenu = this.onContextMenu.bind(this); 51 this.onCopy = this.onCopy.bind(this); 52 this.onCopyAllChanges = this.copyAllChanges.bind(this); 53 this.onCopyDeclaration = this.copyDeclaration.bind(this); 54 this.onCopyRule = this.copyRule.bind(this); 55 this.onClearChanges = this.onClearChanges.bind(this); 56 this.onSelectAll = this.onSelectAll.bind(this); 57 this.onResourceAvailable = this.onResourceAvailable.bind(this); 58 59 this.destroy = this.destroy.bind(this); 60 61 this.init(); 62 } 63 64 get contextMenu() { 65 if (!this._contextMenu) { 66 this._contextMenu = new ChangesContextMenu({ 67 onCopy: this.onCopy, 68 onCopyAllChanges: this.onCopyAllChanges, 69 onCopyDeclaration: this.onCopyDeclaration, 70 onCopyRule: this.onCopyRule, 71 onSelectAll: this.onSelectAll, 72 toolboxDocument: this.inspector.toolbox.doc, 73 window: this.window, 74 }); 75 } 76 77 return this._contextMenu; 78 } 79 80 get resourceCommand() { 81 return this.inspector.commands.resourceCommand; 82 } 83 84 init() { 85 const changesApp = ChangesApp({ 86 onContextMenu: this.onContextMenu, 87 onCopyAllChanges: this.onCopyAllChanges, 88 onCopyRule: this.onCopyRule, 89 }); 90 91 // Expose the provider to let inspector.js use it in setupSidebar. 92 this.provider = createElement( 93 Provider, 94 { 95 id: "changesview", 96 key: "changesview", 97 store: this.store, 98 }, 99 changesApp 100 ); 101 102 this.watchResources(); 103 } 104 105 async watchResources() { 106 await this.resourceCommand.watchResources( 107 [this.resourceCommand.TYPES.DOCUMENT_EVENT], 108 { 109 onAvailable: this.onResourceAvailable, 110 // Ignore any DOCUMENT_EVENT resources that have occured in the past 111 // and are cached by the resource command, otherwise the Changes panel will 112 // react to them erroneously and interpret that the document is reloading *now* 113 // which leads to clearing all stored changes. 114 ignoreExistingResources: true, 115 } 116 ); 117 118 await this.resourceCommand.watchResources( 119 [this.resourceCommand.TYPES.CSS_CHANGE], 120 { onAvailable: this.onResourceAvailable } 121 ); 122 } 123 124 onResourceAvailable(resources) { 125 for (const resource of resources) { 126 if (resource.resourceType === this.resourceCommand.TYPES.CSS_CHANGE) { 127 this.onAddChange(resource); 128 continue; 129 } 130 131 if (resource.name === "dom-loading" && resource.targetFront.isTopLevel) { 132 // will-navigate doesn't work when we navigate to a new process, 133 // and for now, onTargetAvailable/onTargetDestroyed doesn't fire on navigation and 134 // only when navigating to another process. 135 // So we fallback on DOCUMENT_EVENTS to be notified when we navigate. When we 136 // navigate within the same process as well as when we navigate to a new process. 137 // (We would probably revisit that in bug 1632141) 138 this.onClearChanges(); 139 } 140 } 141 } 142 143 /** 144 * Handler for the "Copy All Changes" button. Simple wrapper that just calls 145 * |this.copyChanges()| with no filters in order to trigger default operation. 146 */ 147 copyAllChanges() { 148 this.copyChanges(); 149 } 150 151 /** 152 * Handler for the "Copy Changes" option from the context menu. 153 * Builds a CSS text with the aggregated changes and copies it to the clipboard. 154 * 155 * Optional rule and source ids can be used to filter the scope of the operation: 156 * - if both a rule id and source id are provided, copy only the changes to the 157 * matching rule within the matching source. 158 * - if only a source id is provided, copy the changes to all rules within the 159 * matching source. 160 * - if neither rule id nor source id are provided, copy the changes too all rules 161 * within all sources. 162 * 163 * @param {string | null} ruleId 164 * Optional rule id. 165 * @param {string | null} sourceId 166 * Optional source id. 167 */ 168 copyChanges(ruleId, sourceId) { 169 const state = this.store.getState().changes || {}; 170 const filter = {}; 171 if (ruleId) { 172 filter.ruleIds = [ruleId]; 173 } 174 if (sourceId) { 175 filter.sourceIds = [sourceId]; 176 } 177 178 const text = getChangesStylesheet(state, filter); 179 clipboardHelper.copyString(text); 180 } 181 182 /** 183 * Handler for the "Copy Declaration" option from the context menu. 184 * Builds a CSS declaration string with the property name and value, and copies it 185 * to the clipboard. The declaration is commented out if it is marked as removed. 186 * 187 * @param {DOMElement} element 188 * Host element of a CSS declaration rendered the Changes panel. 189 */ 190 copyDeclaration(element) { 191 const name = element.querySelector( 192 ".changes__declaration-name" 193 ).textContent; 194 const value = element.querySelector( 195 ".changes__declaration-value" 196 ).textContent; 197 const isRemoved = element.classList.contains("diff-remove"); 198 const text = isRemoved ? `/* ${name}: ${value}; */` : `${name}: ${value};`; 199 clipboardHelper.copyString(text); 200 } 201 202 /** 203 * Handler for the "Copy Rule" option from the context menu and "Copy Rule" button. 204 * Gets the full content of the target CSS rule (including any changes applied) 205 * and copies it to the clipboard. 206 * 207 * @param {string} ruleId 208 * Rule id of the target CSS rule. 209 */ 210 async copyRule(ruleId) { 211 const inspectorFronts = await this.inspector.getAllInspectorFronts(); 212 213 for (const inspectorFront of inspectorFronts) { 214 const rule = await inspectorFront.pageStyle.getRule(ruleId); 215 216 if (rule) { 217 const text = await rule.getRuleText(); 218 clipboardHelper.copyString(text); 219 break; 220 } 221 } 222 } 223 224 /** 225 * Handler for the "Copy" option from the context menu. 226 * Copies the current text selection to the clipboard. 227 */ 228 onCopy() { 229 clipboardHelper.copyString(this.window.getSelection().toString()); 230 } 231 232 onAddChange(change) { 233 // Turn data into a suitable change to send to the store. 234 this.store.dispatch(trackChange(change)); 235 } 236 237 onClearChanges() { 238 this.store.dispatch(resetChanges()); 239 } 240 241 /** 242 * Select all text. 243 */ 244 onSelectAll() { 245 const selection = this.window.getSelection(); 246 selection.selectAllChildren( 247 this.document.getElementById("sidebar-panel-changes") 248 ); 249 } 250 251 /** 252 * Event handler for the "contextmenu" event fired when the context menu is requested. 253 * 254 * @param {Event} e 255 */ 256 onContextMenu(e) { 257 this.contextMenu.show(e); 258 } 259 260 /** 261 * Destruction function called when the inspector is destroyed. 262 */ 263 destroy() { 264 this.resourceCommand.unwatchResources( 265 [ 266 this.resourceCommand.TYPES.CSS_CHANGE, 267 this.resourceCommand.TYPES.DOCUMENT_EVENT, 268 ], 269 { onAvailable: this.onResourceAvailable } 270 ); 271 272 this.document = null; 273 this.inspector = null; 274 this.store = null; 275 276 if (this._contextMenu) { 277 this._contextMenu.destroy(); 278 this._contextMenu = null; 279 } 280 } 281 } 282 283 module.exports = ChangesView;