App.js (14354B)
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 "use strict"; 5 6 const { 7 Component, 8 createFactory, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 loader.lazyRequireGetter( 11 this, 12 "PropTypes", 13 "resource://devtools/client/shared/vendor/react-prop-types.js" 14 ); 15 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 16 const { 17 connect, 18 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 19 20 const actions = require("resource://devtools/client/webconsole/actions/index.js"); 21 const { 22 FILTERBAR_DISPLAY_MODES, 23 } = require("resource://devtools/client/webconsole/constants.js"); 24 25 // We directly require Components that we know are going to be used right away 26 const ConsoleOutput = createFactory( 27 require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js") 28 ); 29 const FilterBar = createFactory( 30 require("resource://devtools/client/webconsole/components/FilterBar/FilterBar.js") 31 ); 32 const ReverseSearchInput = createFactory( 33 require("resource://devtools/client/webconsole/components/Input/ReverseSearchInput.js") 34 ); 35 const JSTerm = createFactory( 36 require("resource://devtools/client/webconsole/components/Input/JSTerm.js") 37 ); 38 const ConfirmDialog = createFactory( 39 require("resource://devtools/client/webconsole/components/Input/ConfirmDialog.js") 40 ); 41 const EagerEvaluation = createFactory( 42 require("resource://devtools/client/webconsole/components/Input/EagerEvaluation.js") 43 ); 44 const EvaluationNotification = createFactory( 45 require("resource://devtools/client/webconsole/components/Input/EvaluationNotification.js") 46 ); 47 48 // And lazy load the ones that may not be used. 49 loader.lazyGetter(this, "SideBar", () => 50 createFactory( 51 require("resource://devtools/client/webconsole/components/SideBar.js") 52 ) 53 ); 54 55 loader.lazyGetter(this, "EditorToolbar", () => 56 createFactory( 57 require("resource://devtools/client/webconsole/components/Input/EditorToolbar.js") 58 ) 59 ); 60 61 loader.lazyGetter(this, "NotificationBox", () => 62 createFactory( 63 require("resource://devtools/client/shared/components/NotificationBox.js") 64 .NotificationBox 65 ) 66 ); 67 loader.lazyRequireGetter( 68 this, 69 ["getNotificationWithValue", "PriorityLevels"], 70 "resource://devtools/client/shared/components/NotificationBox.js", 71 true 72 ); 73 74 loader.lazyGetter(this, "GridElementWidthResizer", () => 75 createFactory( 76 require("resource://devtools/client/shared/components/splitter/GridElementWidthResizer.js") 77 ) 78 ); 79 80 loader.lazyGetter(this, "ChromeDebugToolbar", () => 81 createFactory( 82 require("resource://devtools/client/framework/components/ChromeDebugToolbar.js") 83 ) 84 ); 85 86 const l10n = require("resource://devtools/client/webconsole/utils/l10n.js"); 87 const { 88 Utils: WebConsoleUtils, 89 } = require("resource://devtools/client/webconsole/utils.js"); 90 91 const SELF_XSS_OK = l10n.getStr("selfxss.okstring"); 92 const SELF_XSS_MSG = l10n.getFormatStr("selfxss.msg", [SELF_XSS_OK]); 93 94 const { 95 getAllNotifications, 96 } = require("resource://devtools/client/webconsole/selectors/notifications.js"); 97 const { div } = dom; 98 const isMacOS = Services.appinfo.OS === "Darwin"; 99 100 /** 101 * Console root Application component. 102 */ 103 class App extends Component { 104 static get propTypes() { 105 return { 106 dispatch: PropTypes.func.isRequired, 107 webConsoleUI: PropTypes.object.isRequired, 108 notifications: PropTypes.object, 109 onFirstMeaningfulPaint: PropTypes.func.isRequired, 110 serviceContainer: PropTypes.object.isRequired, 111 closeSplitConsole: PropTypes.func.isRequired, 112 autocomplete: PropTypes.bool, 113 currentReverseSearchEntry: PropTypes.string, 114 reverseSearchInputVisible: PropTypes.bool, 115 reverseSearchInitialValue: PropTypes.string, 116 editorMode: PropTypes.bool, 117 editorWidth: PropTypes.number, 118 inputEnabled: PropTypes.bool, 119 sidebarVisible: PropTypes.bool.isRequired, 120 eagerEvaluationEnabled: PropTypes.bool.isRequired, 121 filterBarDisplayMode: PropTypes.oneOf([ 122 ...Object.values(FILTERBAR_DISPLAY_MODES), 123 ]).isRequired, 124 }; 125 } 126 127 constructor(props) { 128 super(props); 129 130 this.onClick = this.onClick.bind(this); 131 this.onPaste = this.onPaste.bind(this); 132 this.onKeyDown = this.onKeyDown.bind(this); 133 this.onBlur = this.onBlur.bind(this); 134 } 135 136 componentDidMount() { 137 window.addEventListener("blur", this.onBlur, { 138 signal: this.#abortController.signal, 139 }); 140 } 141 142 componentWillUnmount() { 143 this.#abortController.abort(); 144 } 145 146 #abortController = new AbortController(); 147 148 onBlur() { 149 this.props.dispatch(actions.autocompleteClear()); 150 } 151 152 onKeyDown(event) { 153 const { dispatch, webConsoleUI } = this.props; 154 155 if ( 156 (!isMacOS && event.key === "F9") || 157 (isMacOS && event.key === "r" && event.ctrlKey === true) 158 ) { 159 const initialValue = 160 webConsoleUI.jsterm && webConsoleUI.jsterm.getSelectedText(); 161 162 dispatch( 163 actions.reverseSearchInputToggle({ initialValue, access: "keyboard" }) 164 ); 165 event.stopPropagation(); 166 // Prevent Reader Mode to be enabled (See Bug 1682340) 167 event.preventDefault(); 168 } 169 170 if ( 171 event.key.toLowerCase() === "b" && 172 ((isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey)) 173 ) { 174 event.stopPropagation(); 175 event.preventDefault(); 176 dispatch(actions.editorToggle()); 177 } 178 } 179 180 onClick(event) { 181 const target = event.originalTarget || event.target; 182 const { reverseSearchInputVisible, dispatch, webConsoleUI } = this.props; 183 184 if ( 185 reverseSearchInputVisible === true && 186 !target.closest(".reverse-search") 187 ) { 188 event.preventDefault(); 189 event.stopPropagation(); 190 dispatch(actions.reverseSearchInputToggle()); 191 return; 192 } 193 194 // Do not focus on middle/right-click or 2+ clicks. 195 if (event.detail !== 1 || event.button !== 0) { 196 return; 197 } 198 199 // Do not focus if a link was clicked 200 if (target.closest("a")) { 201 return; 202 } 203 204 // Do not focus if an input field was clicked 205 if (target.closest("input")) { 206 return; 207 } 208 209 // Do not focus if the click happened in the reverse search toolbar. 210 if (target.closest(".reverse-search")) { 211 return; 212 } 213 214 // Do not focus if something other than the output region was clicked 215 // (including e.g. the clear messages button in toolbar) 216 if (!target.closest(".webconsole-app")) { 217 return; 218 } 219 220 // Do not focus if something is selected 221 const selection = webConsoleUI.document.defaultView.getSelection(); 222 if (selection && !selection.isCollapsed) { 223 return; 224 } 225 226 if (webConsoleUI?.jsterm) { 227 webConsoleUI.jsterm.focus(); 228 } 229 } 230 231 onPaste(event) { 232 const { dispatch, webConsoleUI, notifications } = this.props; 233 234 const { usageCount, CONSOLE_ENTRY_THRESHOLD } = WebConsoleUtils; 235 236 // Bail out if self-xss notification is suppressed. 237 if ( 238 webConsoleUI.isBrowserConsole || 239 usageCount >= CONSOLE_ENTRY_THRESHOLD 240 ) { 241 return; 242 } 243 244 // Stop event propagation, so the clipboard content is *not* inserted. 245 event.preventDefault(); 246 event.stopPropagation(); 247 248 // Bail out if self-xss notification is already there. 249 if (getNotificationWithValue(notifications, "selfxss-notification")) { 250 return; 251 } 252 253 const input = event.target; 254 255 // Cleanup function if notification is closed by the user. 256 const removeCallback = eventType => { 257 if (eventType == "removed") { 258 input.removeEventListener("keyup", pasteKeyUpHandler); 259 dispatch(actions.removeNotification("selfxss-notification")); 260 } 261 }; 262 263 // Create self-xss notification 264 dispatch( 265 actions.appendNotification( 266 SELF_XSS_MSG, 267 "selfxss-notification", 268 null, 269 PriorityLevels.PRIORITY_WARNING_HIGH, 270 null, 271 removeCallback 272 ) 273 ); 274 275 // Remove notification automatically when the user types "allow pasting". 276 const pasteKeyUpHandler = e => { 277 const { value } = e.target; 278 if (value.includes(SELF_XSS_OK)) { 279 dispatch(actions.removeNotification("selfxss-notification")); 280 input.removeEventListener("keyup", pasteKeyUpHandler); 281 WebConsoleUtils.usageCount = WebConsoleUtils.CONSOLE_ENTRY_THRESHOLD; 282 } 283 }; 284 285 input.addEventListener("keyup", pasteKeyUpHandler, { 286 signal: this.#abortController.signal, 287 }); 288 } 289 290 renderChromeDebugToolbar() { 291 const { webConsoleUI } = this.props; 292 if (!webConsoleUI.isBrowserConsole) { 293 return null; 294 } 295 return ChromeDebugToolbar({ 296 // This should always be true at this point 297 isBrowserConsole: webConsoleUI.isBrowserConsole, 298 }); 299 } 300 301 renderFilterBar() { 302 const { closeSplitConsole, filterBarDisplayMode, webConsoleUI } = 303 this.props; 304 305 return FilterBar({ 306 key: "filterbar", 307 closeSplitConsole, 308 displayMode: filterBarDisplayMode, 309 webConsoleUI, 310 }); 311 } 312 313 renderEditorToolbar() { 314 const { 315 editorMode, 316 dispatch, 317 reverseSearchInputVisible, 318 serviceContainer, 319 webConsoleUI, 320 inputEnabled, 321 } = this.props; 322 323 if (!inputEnabled) { 324 return null; 325 } 326 327 return editorMode 328 ? EditorToolbar({ 329 key: "editor-toolbar", 330 editorMode, 331 dispatch, 332 reverseSearchInputVisible, 333 serviceContainer, 334 webConsoleUI, 335 }) 336 : null; 337 } 338 339 renderConsoleOutput() { 340 const { onFirstMeaningfulPaint, serviceContainer, editorMode } = this.props; 341 342 return ConsoleOutput({ 343 key: "console-output", 344 serviceContainer, 345 onFirstMeaningfulPaint, 346 editorMode, 347 }); 348 } 349 350 renderJsTerm() { 351 const { 352 webConsoleUI, 353 serviceContainer, 354 autocomplete, 355 editorMode, 356 editorWidth, 357 inputEnabled, 358 } = this.props; 359 360 return JSTerm({ 361 key: "jsterm", 362 webConsoleUI, 363 serviceContainer, 364 onPaste: this.onPaste, 365 autocomplete, 366 editorMode, 367 editorWidth, 368 inputEnabled, 369 }); 370 } 371 372 renderEagerEvaluation() { 373 const { eagerEvaluationEnabled, serviceContainer, inputEnabled } = 374 this.props; 375 376 if (!eagerEvaluationEnabled || !inputEnabled) { 377 return null; 378 } 379 380 return EagerEvaluation({ serviceContainer }); 381 } 382 383 renderReverseSearch() { 384 const { serviceContainer, reverseSearchInitialValue } = this.props; 385 386 return ReverseSearchInput({ 387 key: "reverse-search-input", 388 setInputValue: serviceContainer.setInputValue, 389 focusInput: serviceContainer.focusInput, 390 initialValue: reverseSearchInitialValue, 391 }); 392 } 393 394 renderSideBar() { 395 const { serviceContainer, sidebarVisible } = this.props; 396 return sidebarVisible 397 ? SideBar({ 398 key: "sidebar", 399 serviceContainer, 400 visible: sidebarVisible, 401 }) 402 : null; 403 } 404 405 renderNotificationBox() { 406 const { notifications, editorMode } = this.props; 407 408 return notifications && notifications.size > 0 409 ? NotificationBox({ 410 id: "webconsole-notificationbox", 411 key: "notification-box", 412 displayBorderTop: !editorMode, 413 displayBorderBottom: editorMode, 414 wrapping: true, 415 notifications, 416 }) 417 : null; 418 } 419 420 renderConfirmDialog() { 421 const { webConsoleUI, serviceContainer } = this.props; 422 423 return ConfirmDialog({ 424 webConsoleUI, 425 serviceContainer, 426 key: "confirm-dialog", 427 }); 428 } 429 430 renderRootElement(children) { 431 const { editorMode, sidebarVisible, inputEnabled, eagerEvaluationEnabled } = 432 this.props; 433 434 const classNames = ["webconsole-app"]; 435 if (sidebarVisible) { 436 classNames.push("sidebar-visible"); 437 } 438 if (editorMode && inputEnabled) { 439 classNames.push("jsterm-editor"); 440 } 441 442 if (eagerEvaluationEnabled && inputEnabled) { 443 classNames.push("eager-evaluation"); 444 } 445 446 return div( 447 { 448 className: classNames.join(" "), 449 onKeyDown: this.onKeyDown, 450 onClick: this.onClick, 451 ref: node => { 452 this.node = node; 453 }, 454 }, 455 children 456 ); 457 } 458 459 render() { 460 const { webConsoleUI, editorMode, dispatch, inputEnabled } = this.props; 461 462 const chromeDebugToolbar = this.renderChromeDebugToolbar(); 463 const filterBar = this.renderFilterBar(); 464 const editorToolbar = this.renderEditorToolbar(); 465 const consoleOutput = this.renderConsoleOutput(); 466 const notificationBox = this.renderNotificationBox(); 467 const jsterm = this.renderJsTerm(); 468 const eager = this.renderEagerEvaluation(); 469 const evaluationNotification = EvaluationNotification(); 470 const reverseSearch = this.renderReverseSearch(); 471 const sidebar = this.renderSideBar(); 472 const confirmDialog = this.renderConfirmDialog(); 473 474 return this.renderRootElement([ 475 chromeDebugToolbar, 476 filterBar, 477 editorToolbar, 478 dom.div( 479 { className: "flexible-output-input", key: "in-out-container" }, 480 consoleOutput, 481 notificationBox, 482 jsterm, 483 eager, 484 evaluationNotification 485 ), 486 editorMode && inputEnabled 487 ? GridElementWidthResizer({ 488 key: "editor-resizer", 489 enabled: editorMode, 490 position: "end", 491 className: "editor-resizer", 492 getControlledElementNode: () => webConsoleUI.jsterm.node, 493 onResizeEnd: width => dispatch(actions.setEditorWidth(width)), 494 }) 495 : null, 496 reverseSearch, 497 sidebar, 498 confirmDialog, 499 ]); 500 } 501 } 502 503 const mapStateToProps = state => ({ 504 notifications: getAllNotifications(state), 505 reverseSearchInputVisible: state.ui.reverseSearchInputVisible, 506 reverseSearchInitialValue: state.ui.reverseSearchInitialValue, 507 editorMode: state.ui.editor, 508 editorWidth: state.ui.editorWidth, 509 sidebarVisible: state.ui.sidebarVisible, 510 filterBarDisplayMode: state.ui.filterBarDisplayMode, 511 eagerEvaluationEnabled: state.prefs.eagerEvaluation, 512 autocomplete: state.prefs.autocomplete, 513 }); 514 515 const mapDispatchToProps = dispatch => ({ 516 dispatch, 517 }); 518 519 module.exports = connect(mapStateToProps, mapDispatchToProps)(App);