CompatibilityView.js (8795B)
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 const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); 16 const LocalizationProvider = createFactory(FluentReact.LocalizationProvider); 17 18 const compatibilityReducer = require("resource://devtools/client/inspector/compatibility/reducers/compatibility.js"); 19 const { 20 appendNode, 21 clearDestroyedNodes, 22 initUserSettings, 23 removeNode, 24 updateNodes, 25 updateSelectedNode, 26 updateTopLevelTarget, 27 updateNode, 28 } = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js"); 29 30 const CompatibilityApp = createFactory( 31 require("resource://devtools/client/inspector/compatibility/components/CompatibilityApp.js") 32 ); 33 34 class CompatibilityView { 35 constructor(inspector) { 36 this.inspector = inspector; 37 38 this.inspector.store.injectReducer("compatibility", compatibilityReducer); 39 40 this.#init(); 41 } 42 43 #isChangeAddedWhileHidden; 44 #previousChangedSelector; 45 #updateNodesTimeoutId; 46 47 destroy() { 48 try { 49 this.resourceCommand.unwatchResources( 50 [this.resourceCommand.TYPES.CSS_CHANGE], 51 { 52 onAvailable: this.#onResourceAvailable, 53 } 54 ); 55 } catch (e) { 56 // If unwatchResources is called before finishing process of watchResources, 57 // unwatchResources throws an error during stopping listener. 58 } 59 60 this.inspector.off("new-root", this.#onTopLevelTargetChanged); 61 this.inspector.off("markupmutation", this.#onMarkupMutation); 62 this.inspector.selection.off("new-node-front", this.#onSelectedNodeChanged); 63 this.inspector.sidebar.off( 64 "compatibilityview-selected", 65 this.#onPanelSelected 66 ); 67 this.inspector = null; 68 } 69 70 get resourceCommand() { 71 return this.inspector.commands.resourceCommand; 72 } 73 74 async #init() { 75 const { setSelectedNode } = this.inspector.getCommonComponentProps(); 76 const compatibilityApp = new CompatibilityApp({ 77 setSelectedNode, 78 }); 79 80 this.provider = createElement( 81 Provider, 82 { 83 id: "compatibilityview", 84 store: this.inspector.store, 85 }, 86 LocalizationProvider( 87 { 88 bundles: this.inspector.fluentL10n.getBundles(), 89 parseMarkup: this.#parseMarkup, 90 }, 91 compatibilityApp 92 ) 93 ); 94 95 await this.inspector.store.dispatch(initUserSettings()); 96 // awaiting for `initUserSettings` makes us miss the initial "compatibilityview-selected" 97 // event, so we need to manually call #onPanelSelected to fetch compatibility issues 98 // for the selected node (and the whole page). 99 this.#onPanelSelected(); 100 101 this.inspector.on("new-root", this.#onTopLevelTargetChanged); 102 this.inspector.on("markupmutation", this.#onMarkupMutation); 103 this.inspector.selection.on("new-node-front", this.#onSelectedNodeChanged); 104 this.inspector.sidebar.on( 105 "compatibilityview-selected", 106 this.#onPanelSelected 107 ); 108 109 await this.resourceCommand.watchResources( 110 [this.resourceCommand.TYPES.CSS_CHANGE], 111 { 112 onAvailable: this.#onResourceAvailable, 113 // CSS changes made before opening Compatibility View are already applied to 114 // corresponding DOM at this point, so existing resources can be ignored here. 115 ignoreExistingResources: true, 116 } 117 ); 118 119 this.inspector.emitForTests("compatibilityview-initialized"); 120 } 121 122 #isAvailable() { 123 return ( 124 this.inspector && 125 this.inspector.sidebar && 126 this.inspector.sidebar.getCurrentTabID() === "compatibilityview" && 127 this.inspector.selection && 128 this.inspector.selection.isConnected() 129 ); 130 } 131 132 #parseMarkup = () => { 133 // Using a BrowserLoader for the inspector is currently blocked on performance regressions, 134 // see Bug 1471853. 135 throw new Error( 136 "The inspector cannot use tags in ftl strings because it does not run in a BrowserLoader" 137 ); 138 }; 139 140 #onChangeAdded = ({ selector }) => { 141 if (!this.#isAvailable()) { 142 // In order to update this panel if a change is added while hiding this panel. 143 this.#isChangeAddedWhileHidden = true; 144 return; 145 } 146 147 this.#isChangeAddedWhileHidden = false; 148 149 // We need to debounce updating nodes since we might get CSS_CHANGE resources for 150 // every typed character until fixing bug 1503036. 151 if (this.#previousChangedSelector === selector) { 152 clearTimeout(this.#updateNodesTimeoutId); 153 } 154 this.#previousChangedSelector = selector; 155 156 this.#updateNodesTimeoutId = setTimeout(() => { 157 // TODO: In case of keyframes changes, the selector given from changes actor is 158 // keyframe-selector such as "from" and "100%", not selector for node. Thus, 159 // we need to address this case. 160 this.inspector.store.dispatch(updateNodes(selector)); 161 }, 500); 162 }; 163 164 #onMarkupMutation = mutations => { 165 // Since the mutations are throttled (in WalkerActor#getMutations), we might get the 166 // same nodeFront multiple times. 167 // Put them in a Set so we don't call updateNode more than once for a given front. 168 const targetsWithAttributeMutation = new Set(); 169 const childListMutation = []; 170 171 for (const mutation of mutations) { 172 if ( 173 mutation.type === "attributes" && 174 (mutation.attributeName === "style" || 175 mutation.attributeName === "class") 176 ) { 177 targetsWithAttributeMutation.add(mutation.target); 178 } 179 if (mutation.type === "childList") { 180 childListMutation.push(mutation); 181 } 182 } 183 184 if ( 185 targetsWithAttributeMutation.size === 0 && 186 childListMutation.length === 0 187 ) { 188 return; 189 } 190 191 if (!this.#isAvailable()) { 192 // In order to update this panel if a change is added while hiding this panel. 193 this.#isChangeAddedWhileHidden = true; 194 return; 195 } 196 197 this.#isChangeAddedWhileHidden = false; 198 199 // Resource Watcher doesn't respond to programmatic inline CSS 200 // change. This check can be removed once the following bug is resolved 201 // https://bugzilla.mozilla.org/show_bug.cgi?id=1506160 202 for (const target of targetsWithAttributeMutation) { 203 this.inspector.store.dispatch(updateNode(target)); 204 } 205 206 // Destroyed nodes can be cleaned up 207 // once at the end if necessary 208 let cleanupDestroyedNodes = false; 209 for (const { removed, target } of childListMutation) { 210 if (!removed.length) { 211 this.inspector.store.dispatch(appendNode(target)); 212 continue; 213 } 214 215 const retainedNodes = removed.filter(node => node && !node.isDestroyed()); 216 cleanupDestroyedNodes = 217 cleanupDestroyedNodes || retainedNodes.length !== removed.length; 218 219 for (const retainedNode of retainedNodes) { 220 this.inspector.store.dispatch(removeNode(retainedNode)); 221 } 222 } 223 224 if (cleanupDestroyedNodes) { 225 this.inspector.store.dispatch(clearDestroyedNodes()); 226 } 227 }; 228 229 #onPanelSelected = () => { 230 const { selectedNode, topLevelTarget } = 231 this.inspector.store.getState().compatibility; 232 233 // Update if the selected node is changed or new change is added while the panel was hidden. 234 if ( 235 this.inspector.selection.nodeFront !== selectedNode || 236 this.#isChangeAddedWhileHidden 237 ) { 238 this.#onSelectedNodeChanged(); 239 } 240 241 // Update if the top target has changed or new change is added while the panel was hidden. 242 if ( 243 this.inspector.toolbox.target !== topLevelTarget || 244 this.#isChangeAddedWhileHidden 245 ) { 246 this.#onTopLevelTargetChanged(); 247 } 248 249 this.#isChangeAddedWhileHidden = false; 250 }; 251 252 #onSelectedNodeChanged = () => { 253 if (!this.#isAvailable()) { 254 return; 255 } 256 257 this.inspector.store.dispatch( 258 updateSelectedNode(this.inspector.selection.nodeFront) 259 ); 260 }; 261 262 #onResourceAvailable = resources => { 263 for (const resource of resources) { 264 // Style changes applied inline directly to 265 // the element and its changes are monitored by 266 // #onMarkupMutation via markupmutation events. 267 // Hence those changes can be ignored here 268 if (resource.source?.type !== "element") { 269 this.#onChangeAdded(resource); 270 } 271 } 272 }; 273 274 #onTopLevelTargetChanged = () => { 275 if (!this.#isAvailable()) { 276 return; 277 } 278 279 this.inspector.store.dispatch( 280 updateTopLevelTarget(this.inspector.toolbox.target) 281 ); 282 }; 283 } 284 285 module.exports = CompatibilityView;