Files
firefox/devtools/client/webconsole/components/ConsoleOutput.js
T

228 lines
7.4 KiB
JavaScript

/* 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 { Component, createFactory } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
const {initialize} = require("devtools/client/webconsole/actions/ui");
const {sortBy} = require("devtools/client/shared/vendor/lodash");
const {
getAllMessagesById,
getAllMessagesUiById,
getAllMessagesTableDataById,
getAllNetworkMessagesUpdateById,
getVisibleMessages,
getPausedExecutionPoint,
getAllRepeatById,
} = require("devtools/client/webconsole/selectors/messages");
const MessageContainer = createFactory(require("devtools/client/webconsole/components/MessageContainer").MessageContainer);
const {
MESSAGE_TYPE,
} = require("devtools/client/webconsole/constants");
const {
getInitialMessageCountForViewport,
} = require("devtools/client/webconsole/utils/messages.js");
function getClosestMessage(visibleMessages, messages, executionPoint) {
if (!executionPoint || !visibleMessages) {
return null;
}
const { progress } = executionPoint;
const getProgress = m => m && m.executionPoint && m.executionPoint.progress;
return sortBy(
visibleMessages.map(id => messages.get(id)),
m => Math.abs(progress - getProgress(m))
)[0];
}
class ConsoleOutput extends Component {
static get propTypes() {
return {
initialized: PropTypes.bool.isRequired,
messages: PropTypes.object.isRequired,
messagesUi: PropTypes.array.isRequired,
serviceContainer: PropTypes.shape({
attachRefToHud: PropTypes.func.isRequired,
openContextMenu: PropTypes.func.isRequired,
sourceMapService: PropTypes.object,
}),
dispatch: PropTypes.func.isRequired,
timestampsVisible: PropTypes.bool,
messagesTableData: PropTypes.object.isRequired,
messagesRepeat: PropTypes.object.isRequired,
networkMessagesUpdate: PropTypes.object.isRequired,
visibleMessages: PropTypes.array.isRequired,
networkMessageActiveTabId: PropTypes.string.isRequired,
onFirstMeaningfulPaint: PropTypes.func.isRequired,
pausedExecutionPoint: PropTypes.any,
};
}
constructor(props) {
super(props);
this.onContextMenu = this.onContextMenu.bind(this);
this.maybeScrollToBottom = this.maybeScrollToBottom.bind(this);
}
componentDidMount() {
scrollToBottom(this.outputNode);
this.props.serviceContainer.attachRefToHud("outputScroller", this.outputNode);
// Waiting for the next paint.
new Promise(res => requestAnimationFrame(res))
.then(() => {
if (this.props.onFirstMeaningfulPaint) {
this.props.onFirstMeaningfulPaint();
}
// Dispatching on next tick so we don't block on action execution.
setTimeout(() => {
this.props.dispatch(initialize());
}, 0);
});
}
componentWillUpdate(nextProps, nextState) {
const outputNode = this.outputNode;
if (!outputNode || !outputNode.lastChild) {
// Force a scroll to bottom when messages are added to an empty console.
// This makes the console stay pinned to the bottom if a batch of messages
// are added after a page refresh (Bug 1402237).
this.shouldScrollBottom = true;
return;
}
const lastChild = outputNode.lastChild;
const visibleMessagesDelta =
nextProps.visibleMessages.length - this.props.visibleMessages.length;
const messagesDelta =
nextProps.messages.size - this.props.messages.size;
// We need to scroll to the bottom if:
// - we are reacting to the "initialize" action,
// and we are already scrolled to the bottom
// - the number of messages displayed changed
// and we are already scrolled to the bottom
// - the number of messages in the store changed
// and the new message is an evaluation result.
const isNewMessageEvaluationResult = messagesDelta > 0 &&
[...nextProps.messages.values()][nextProps.messages.size - 1].type
=== MESSAGE_TYPE.RESULT;
this.shouldScrollBottom =
(
!this.props.initialized &&
nextProps.initialized &&
isScrolledToBottom(lastChild, outputNode)
) ||
(isNewMessageEvaluationResult) ||
(visibleMessagesDelta > 0 && isScrolledToBottom(lastChild, outputNode));
}
componentDidUpdate() {
this.maybeScrollToBottom();
}
maybeScrollToBottom() {
if (this.outputNode && this.shouldScrollBottom) {
scrollToBottom(this.outputNode);
}
}
onContextMenu(e) {
this.props.serviceContainer.openContextMenu(e);
e.stopPropagation();
e.preventDefault();
}
render() {
let {
dispatch,
visibleMessages,
messages,
messagesUi,
messagesTableData,
messagesRepeat,
networkMessagesUpdate,
networkMessageActiveTabId,
serviceContainer,
timestampsVisible,
initialized,
pausedExecutionPoint,
} = this.props;
if (!initialized) {
const numberMessagesFitViewport = getInitialMessageCountForViewport(window);
if (numberMessagesFitViewport < visibleMessages.length) {
visibleMessages = visibleMessages.slice(
visibleMessages.length - numberMessagesFitViewport);
}
}
const pausedMessage = getClosestMessage(
visibleMessages, messages, pausedExecutionPoint);
const messageNodes = visibleMessages.map((messageId) => MessageContainer({
dispatch,
key: messageId,
messageId,
serviceContainer,
open: messagesUi.includes(messageId),
tableData: messagesTableData.get(messageId),
timestampsVisible,
repeat: messagesRepeat[messageId],
networkMessageUpdate: networkMessagesUpdate[messageId],
networkMessageActiveTabId,
pausedExecutionPoint,
getMessage: () => messages.get(messageId),
isPaused: pausedMessage && pausedMessage.id == messageId,
maybeScrollToBottom: this.maybeScrollToBottom,
}));
return (
dom.div({
className: "webconsole-output",
onContextMenu: this.onContextMenu,
ref: node => {
this.outputNode = node;
},
}, messageNodes
)
);
}
}
function scrollToBottom(node) {
node.scrollTop = node.scrollHeight;
}
function isScrolledToBottom(outputNode, scrollNode) {
const lastNodeHeight = outputNode.lastChild ?
outputNode.lastChild.clientHeight : 0;
return scrollNode.scrollTop + scrollNode.clientHeight >=
scrollNode.scrollHeight - lastNodeHeight / 2;
}
function mapStateToProps(state, props) {
return {
initialized: state.ui.initialized,
pausedExecutionPoint: getPausedExecutionPoint(state),
messages: getAllMessagesById(state),
visibleMessages: getVisibleMessages(state),
messagesUi: getAllMessagesUiById(state),
messagesTableData: getAllMessagesTableDataById(state),
messagesRepeat: getAllRepeatById(state),
networkMessagesUpdate: getAllNetworkMessagesUpdateById(state),
timestampsVisible: state.ui.timestampsVisible,
networkMessageActiveTabId: state.ui.networkMessageActiveTabId,
};
}
module.exports = connect(mapStateToProps)(ConsoleOutput);