diff options
Diffstat (limited to 'devtools/client/webconsole/components/Output/Message.js')
-rw-r--r-- | devtools/client/webconsole/components/Output/Message.js | 482 |
1 files changed, 482 insertions, 0 deletions
diff --git a/devtools/client/webconsole/components/Output/Message.js b/devtools/client/webconsole/components/Output/Message.js new file mode 100644 index 0000000000..ee65c8947a --- /dev/null +++ b/devtools/client/webconsole/components/Output/Message.js @@ -0,0 +1,482 @@ +/* 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"; + +// React & Redux +const { + Component, + createFactory, + createElement, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { + l10n, +} = require("resource://devtools/client/webconsole/utils/messages.js"); +const actions = require("resource://devtools/client/webconsole/actions/index.js"); +const { + MESSAGE_LEVEL, + MESSAGE_SOURCE, + MESSAGE_TYPE, +} = require("resource://devtools/client/webconsole/constants.js"); +const { + MessageIndent, +} = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js"); +const MessageIcon = require("resource://devtools/client/webconsole/components/Output/MessageIcon.js"); +const FrameView = createFactory( + require("resource://devtools/client/shared/components/Frame.js") +); + +loader.lazyRequireGetter( + this, + "CollapseButton", + "resource://devtools/client/webconsole/components/Output/CollapseButton.js" +); +loader.lazyRequireGetter( + this, + "MessageRepeat", + "resource://devtools/client/webconsole/components/Output/MessageRepeat.js" +); +loader.lazyRequireGetter( + this, + "PropTypes", + "resource://devtools/client/shared/vendor/react-prop-types.js" +); +loader.lazyRequireGetter( + this, + "SmartTrace", + "resource://devtools/client/shared/components/SmartTrace.js" +); + +class Message extends Component { + static get propTypes() { + return { + open: PropTypes.bool, + collapsible: PropTypes.bool, + collapseTitle: PropTypes.string, + disabled: PropTypes.bool, + onToggle: PropTypes.func, + source: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + level: PropTypes.string.isRequired, + indent: PropTypes.number.isRequired, + inWarningGroup: PropTypes.bool, + isBlockedNetworkMessage: PropTypes.bool, + topLevelClasses: PropTypes.array.isRequired, + messageBody: PropTypes.any.isRequired, + repeat: PropTypes.any, + frame: PropTypes.any, + attachment: PropTypes.any, + stacktrace: PropTypes.any, + messageId: PropTypes.string, + scrollToMessage: PropTypes.bool, + exceptionDocURL: PropTypes.string, + request: PropTypes.object, + dispatch: PropTypes.func, + timeStamp: PropTypes.number, + timestampsVisible: PropTypes.bool.isRequired, + serviceContainer: PropTypes.shape({ + emitForTests: PropTypes.func.isRequired, + onViewSource: PropTypes.func.isRequired, + onViewSourceInDebugger: PropTypes.func, + onViewSourceInStyleEditor: PropTypes.func, + openContextMenu: PropTypes.func.isRequired, + openLink: PropTypes.func.isRequired, + sourceMapURLService: PropTypes.any, + preventStacktraceInitialRenderDelay: PropTypes.bool, + }), + notes: PropTypes.arrayOf( + PropTypes.shape({ + messageBody: PropTypes.string.isRequired, + frame: PropTypes.any, + }) + ), + maybeScrollToBottom: PropTypes.func, + message: PropTypes.object.isRequired, + }; + } + + static get defaultProps() { + return { + indent: 0, + }; + } + + constructor(props) { + super(props); + this.onLearnMoreClick = this.onLearnMoreClick.bind(this); + this.toggleMessage = this.toggleMessage.bind(this); + this.onContextMenu = this.onContextMenu.bind(this); + this.renderIcon = this.renderIcon.bind(this); + } + + componentDidMount() { + if (this.messageNode) { + if (this.props.scrollToMessage) { + this.messageNode.scrollIntoView(); + } + + this.emitNewMessage(this.messageNode); + } + } + + componentDidCatch(e) { + this.setState({ error: e }); + } + + // Event used in tests. Some message types don't pass it in because existing tests + // did not emit for them. + emitNewMessage(node) { + const { serviceContainer, messageId, timeStamp } = this.props; + serviceContainer.emitForTests( + "new-messages", + new Set([{ node, messageId, timeStamp }]) + ); + } + + onLearnMoreClick(e) { + const { exceptionDocURL } = this.props; + this.props.serviceContainer.openLink(exceptionDocURL, e); + e.preventDefault(); + } + + toggleMessage(e) { + // Don't bubble up to the main App component, which redirects focus to input, + // making difficult for screen reader users to review output + e.stopPropagation(); + const { open, dispatch, messageId, onToggle, disabled } = this.props; + + if (disabled) { + return; + } + + // Early exit the function to avoid the message to collapse if the user is + // selecting a range in the toggle message. + const window = e.target.ownerDocument.defaultView; + if (window.getSelection && window.getSelection().type === "Range") { + return; + } + + // If defined on props, we let the onToggle() method handle the toggling, + // otherwise we toggle the message open/closed ourselves. + if (onToggle) { + onToggle(messageId, e); + } else if (open) { + dispatch(actions.messageClose(messageId)); + } else { + dispatch(actions.messageOpen(messageId)); + } + } + + onContextMenu(e) { + const { serviceContainer, source, request, messageId } = this.props; + const messageInfo = { + source, + request, + messageId, + }; + serviceContainer.openContextMenu(e, messageInfo); + e.stopPropagation(); + e.preventDefault(); + } + + renderIcon() { + const { level, inWarningGroup, isBlockedNetworkMessage, type, disabled } = + this.props; + + if (inWarningGroup) { + return undefined; + } + + if (disabled) { + return MessageIcon({ + level: MESSAGE_LEVEL.INFO, + type, + title: l10n.getStr("webconsole.disableIcon.title"), + }); + } + + if (isBlockedNetworkMessage) { + return MessageIcon({ + level: MESSAGE_LEVEL.ERROR, + type: "blockedReason", + }); + } + + return MessageIcon({ + level, + type, + }); + } + + renderTimestamp() { + if (!this.props.timestampsVisible) { + return null; + } + + return dom.span( + { + className: "timestamp devtools-monospace", + }, + l10n.timestampString(this.props.timeStamp || Date.now()) + ); + } + + renderErrorState() { + const newBugUrl = + "https://bugzilla.mozilla.org/enter_bug.cgi?product=DevTools&component=Console"; + const timestampEl = this.renderTimestamp(); + + return dom.div( + { + className: "message error message-did-catch", + }, + timestampEl, + MessageIcon({ level: "error" }), + dom.span( + { className: "message-body-wrapper" }, + dom.span( + { + className: "message-flex-body", + }, + // Add whitespaces for formatting when copying to the clipboard. + timestampEl ? " " : null, + dom.span( + { className: "message-body devtools-monospace" }, + l10n.getFormatStr("webconsole.message.componentDidCatch.label", [ + newBugUrl, + ]), + dom.button( + { + className: "devtools-button", + onClick: () => + navigator.clipboard.writeText( + JSON.stringify( + this.props.message, + function (key, value) { + // The message can hold one or multiple fronts that we need to serialize + if (value?.getGrip) { + return value.getGrip(); + } + return value; + }, + 2 + ) + ), + }, + l10n.getStr( + "webconsole.message.componentDidCatch.copyButton.label" + ) + ) + ) + ) + ), + dom.br() + ); + } + + // eslint-disable-next-line complexity + render() { + if (this.state && this.state.error) { + return this.renderErrorState(); + } + + const { + open, + collapsible, + collapseTitle, + disabled, + source, + type, + level, + indent, + inWarningGroup, + topLevelClasses, + messageBody, + frame, + stacktrace, + serviceContainer, + exceptionDocURL, + messageId, + notes, + } = this.props; + + topLevelClasses.push("message", source, type, level); + if (open) { + topLevelClasses.push("open"); + } + + if (disabled) { + topLevelClasses.push("disabled"); + } + + const timestampEl = this.renderTimestamp(); + const icon = this.renderIcon(); + + // Figure out if there is an expandable part to the message. + let attachment = null; + if (this.props.attachment) { + attachment = this.props.attachment; + } else if (stacktrace && open) { + const smartTraceAttributes = { + stacktrace, + onViewSourceInDebugger: + serviceContainer.onViewSourceInDebugger || + serviceContainer.onViewSource, + onViewSource: serviceContainer.onViewSource, + onReady: this.props.maybeScrollToBottom, + sourceMapURLService: serviceContainer.sourceMapURLService, + }; + + if (serviceContainer.preventStacktraceInitialRenderDelay) { + smartTraceAttributes.initialRenderDelay = 0; + } + + attachment = dom.div( + { + className: "stacktrace devtools-monospace", + }, + createElement(SmartTrace, smartTraceAttributes) + ); + } + + // If there is an expandable part, make it collapsible. + let collapse = null; + if (collapsible && !disabled) { + collapse = createElement(CollapseButton, { + open, + title: collapseTitle, + onClick: this.toggleMessage, + }); + } + + let notesNodes; + if (notes) { + notesNodes = notes.map(note => + dom.span( + { className: "message-flex-body error-note" }, + dom.span( + { className: "message-body devtools-monospace" }, + "note: " + note.messageBody + ), + dom.span( + { className: "message-location devtools-monospace" }, + note.frame + ? FrameView({ + frame: note.frame, + onClick: serviceContainer + ? serviceContainer.onViewSourceInDebugger || + serviceContainer.onViewSource + : undefined, + showEmptyPathAsHost: true, + sourceMapURLService: serviceContainer + ? serviceContainer.sourceMapURLService + : undefined, + }) + : null + ) + ) + ); + } else { + notesNodes = []; + } + + const repeat = + this.props.repeat && this.props.repeat > 1 + ? createElement(MessageRepeat, { repeat: this.props.repeat }) + : null; + + let onFrameClick; + if (serviceContainer && frame) { + if (source === MESSAGE_SOURCE.CSS) { + onFrameClick = + serviceContainer.onViewSourceInStyleEditor || + serviceContainer.onViewSource; + } else { + // Point everything else to debugger, if source not available, + // it will fall back to view-source. + onFrameClick = + serviceContainer.onViewSourceInDebugger || + serviceContainer.onViewSource; + } + } + + // Configure the location. + const location = frame + ? FrameView({ + className: "message-location devtools-monospace", + frame, + onClick: onFrameClick, + showEmptyPathAsHost: true, + sourceMapURLService: serviceContainer + ? serviceContainer.sourceMapURLService + : undefined, + messageSource: source, + }) + : null; + + let learnMore; + if (exceptionDocURL) { + learnMore = dom.a( + { + className: "learn-more-link webconsole-learn-more-link", + href: exceptionDocURL, + title: exceptionDocURL.split("?")[0], + onClick: this.onLearnMoreClick, + }, + `[${l10n.getStr("webConsoleMoreInfoLabel")}]` + ); + } + + const bodyElements = Array.isArray(messageBody) + ? messageBody + : [messageBody]; + + return dom.div( + { + className: topLevelClasses.join(" "), + onContextMenu: this.onContextMenu, + ref: node => { + this.messageNode = node; + }, + "data-message-id": messageId, + "data-indent": indent || 0, + "aria-live": type === MESSAGE_TYPE.COMMAND ? "off" : "polite", + }, + timestampEl, + MessageIndent({ + indent, + inWarningGroup, + }), + this.props.isBlockedNetworkMessage ? collapse : icon, + this.props.isBlockedNetworkMessage ? icon : collapse, + dom.span( + { className: "message-body-wrapper" }, + dom.span( + { + className: "message-flex-body", + onClick: collapsible ? this.toggleMessage : undefined, + }, + // Add whitespaces for formatting when copying to the clipboard. + timestampEl ? " " : null, + dom.span( + { className: "message-body devtools-monospace" }, + ...bodyElements, + learnMore + ), + repeat ? " " : null, + repeat, + " ", + location + ), + attachment, + ...notesNodes + ), + // If an attachment is displayed, the final newline is handled by the attachment. + attachment ? null : dom.br() + ); + } +} + +module.exports = Message; |