Bug 1306099 - New console frontend: Fix scrolling. r=bgrins
authorLin Clark <lclark@mozilla.com>
Sun, 02 Oct 2016 15:16:49 -0700
changeset 316306 4a08689beee88bc554f0b26ebb1b89a6fec85985
parent 316305 cf5c96ed4ae6d6d6968a5e1bf4f97a2bb070500a
child 316307 56cb28b02f39f89b2b90622e2c2c8324d0d6ad91
push id30768
push usercbook@mozilla.com
push dateTue, 04 Oct 2016 09:56:53 +0000
treeherdermozilla-central@6bfa0e8a9f20 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1306099
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1306099 - New console frontend: Fix scrolling. r=bgrins MozReview-Commit-ID: 3xRTUTh53Bp
devtools/client/webconsole/new-console-output/components/console-output.js
devtools/client/webconsole/new-console-output/components/message-container.js
devtools/client/webconsole/new-console-output/components/message-types/console-command.js
devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
devtools/client/webconsole/new-console-output/components/message.js
devtools/client/webconsole/new-console-output/reducers/ui.js
devtools/client/webconsole/new-console-output/selectors/ui.js
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -8,49 +8,62 @@ const {
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const { getAllMessages, getAllMessagesUiById, getAllMessagesTableDataById } = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const { getScrollSetting } = require("devtools/client/webconsole/new-console-output/selectors/ui");
 const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
 
 const ConsoleOutput = createClass({
 
   displayName: "ConsoleOutput",
 
   propTypes: {
     hudProxyClient: PropTypes.object.isRequired,
     messages: PropTypes.object.isRequired,
     messagesUi: PropTypes.object.isRequired,
     sourceMapService: PropTypes.object,
     onViewSourceInDebugger: PropTypes.func.isRequired,
     openNetworkPanel: PropTypes.func.isRequired,
     openLink: PropTypes.func.isRequired,
+    autoscroll: PropTypes.bool.isRequired,
+  },
+
+  componentDidMount() {
+    scrollToBottom(this.outputNode);
   },
 
-  componentWillUpdate() {
-    let node = ReactDOM.findDOMNode(this);
-    if (node.lastChild) {
-      this.shouldScrollBottom = isScrolledToBottom(node.lastChild, node);
+  componentWillUpdate(nextProps, nextState) {
+    if (!this.outputNode) {
+      return;
+    }
+
+    const outputNode = this.outputNode;
+
+    // Figure out if we are at the bottom. If so, then any new message should be scrolled
+    // into view.
+    if (this.props.autoscroll && outputNode.lastChild) {
+      this.shouldScrollBottom = isScrolledToBottom(outputNode.lastChild, outputNode);
     }
   },
 
   componentDidUpdate() {
     if (this.shouldScrollBottom) {
-      let node = ReactDOM.findDOMNode(this);
-      node.scrollTop = node.scrollHeight;
+      scrollToBottom(this.outputNode);
     }
   },
 
   render() {
     let {
       dispatch,
+      autoscroll,
       hudProxyClient,
       messages,
       messagesUi,
       messagesTableData,
       sourceMapService,
       onViewSourceInDebugger,
       openNetworkPanel,
       openLink,
@@ -64,33 +77,45 @@ const ConsoleOutput = createClass({
           message,
           key: message.id,
           sourceMapService,
           onViewSourceInDebugger,
           openNetworkPanel,
           openLink,
           open: messagesUi.includes(message.id),
           tableData: messagesTableData.get(message.id),
+          autoscroll,
         })
       );
     });
     return (
-      dom.div({className: "webconsole-output"}, messageNodes)
+      dom.div({
+        className: "webconsole-output",
+        ref: node => {
+          this.outputNode = node;
+        },
+      }, messageNodes
+      )
     );
   }
 });
 
+function scrollToBottom(node) {
+  node.scrollTop = node.scrollHeight;
+}
+
 function isScrolledToBottom(outputNode, scrollNode) {
   let lastNodeHeight = outputNode.lastChild ?
                        outputNode.lastChild.clientHeight : 0;
   return scrollNode.scrollTop + scrollNode.clientHeight >=
          scrollNode.scrollHeight - lastNodeHeight / 2;
 }
 
 function mapStateToProps(state) {
   return {
     messages: getAllMessages(state),
     messagesUi: getAllMessagesUiById(state),
     messagesTableData: getAllMessagesTableDataById(state),
+    autoscroll: getScrollSetting(state),
   };
 }
 
 module.exports = connect(mapStateToProps)(ConsoleOutput);
--- a/devtools/client/webconsole/new-console-output/components/message-container.js
+++ b/devtools/client/webconsole/new-console-output/components/message-container.js
@@ -33,56 +33,37 @@ const MessageContainer = createClass({
   propTypes: {
     message: PropTypes.object.isRequired,
     sourceMapService: PropTypes.object,
     onViewSourceInDebugger: PropTypes.func.isRequired,
     openNetworkPanel: PropTypes.func.isRequired,
     openLink: PropTypes.func.isRequired,
     open: PropTypes.bool.isRequired,
     hudProxyClient: PropTypes.object.isRequired,
+    autoscroll: PropTypes.bool.isRequired,
   },
 
   getDefaultProps: function () {
     return {
       open: false
     };
   },
 
   shouldComponentUpdate(nextProps, nextState) {
     const repeatChanged = this.props.message.repeat !== nextProps.message.repeat;
     const openChanged = this.props.open !== nextProps.open;
     const tableDataChanged = this.props.tableData !== nextProps.tableData;
     return repeatChanged || openChanged || tableDataChanged;
   },
 
   render() {
-    const {
-      dispatch,
-      message,
-      sourceMapService,
-      onViewSourceInDebugger,
-      openNetworkPanel,
-      openLink,
-      open,
-      tableData,
-      hudProxyClient,
-    } = this.props;
+    const { message } = this.props;
 
     let MessageComponent = createFactory(getMessageComponent(message));
-    return MessageComponent({
-      dispatch,
-      message,
-      sourceMapService,
-      onViewSourceInDebugger,
-      openNetworkPanel,
-      openLink,
-      open,
-      tableData,
-      hudProxyClient,
-    });
+    return MessageComponent(this.props);
   }
 });
 
 function getMessageComponent(message) {
   switch (message.source) {
     case MESSAGE_SOURCE.CONSOLE_API:
       return componentMap.get("ConsoleApiCall");
     case MESSAGE_SOURCE.NETWORK:
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
@@ -12,31 +12,33 @@ const {
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const Message = createFactory(require("devtools/client/webconsole/new-console-output/components/message"));
 
 ConsoleCommand.displayName = "ConsoleCommand";
 
 ConsoleCommand.propTypes = {
   message: PropTypes.object.isRequired,
+  autoscroll: PropTypes.bool.isRequired,
 };
 
 /**
  * Displays input from the console.
  */
 function ConsoleCommand(props) {
   const {
     source,
     type,
     level,
-    messageText: messageBody
+    messageText: messageBody,
   } = props.message;
 
   const childProps = {
     source,
     type,
     level,
-    messageBody
+    messageBody,
+    scrollToMessage: props.autoscroll,
   };
   return Message(childProps);
 }
 
 module.exports = ConsoleCommand;
--- a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
@@ -38,13 +38,14 @@ function EvaluationResult(props) {
   const topLevelClasses = ["cm-s-mozilla"];
 
   const childProps = {
     source,
     type,
     level,
     topLevelClasses,
     messageBody,
+    scrollToMessage: props.autoscroll,
   };
   return Message(childProps);
 }
 
 module.exports = EvaluationResult;
--- a/devtools/client/webconsole/new-console-output/components/message.js
+++ b/devtools/client/webconsole/new-console-output/components/message.js
@@ -3,123 +3,140 @@
 /* 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 {
+  createClass,
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button"));
 const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon"));
 const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat"));
 const FrameView = createFactory(require("devtools/client/shared/components/frame"));
 const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
 
-Message.displayName = "Message";
+const Message = createClass({
+  displayName: "Message",
 
-Message.propTypes = {
-  open: PropTypes.bool,
-  source: PropTypes.string.isRequired,
-  type: PropTypes.string.isRequired,
-  level: PropTypes.string.isRequired,
-  topLevelClasses: PropTypes.array,
-  messageBody: PropTypes.any.isRequired,
-  repeat: PropTypes.any,
-  frame: PropTypes.any,
-  attachment: PropTypes.any,
-  stacktrace: PropTypes.any,
-  messageId: PropTypes.string,
-  onViewSourceInDebugger: PropTypes.func,
-  sourceMapService: PropTypes.any,
-};
+  propTypes: {
+    open: PropTypes.bool,
+    source: PropTypes.string.isRequired,
+    type: PropTypes.string.isRequired,
+    level: PropTypes.string.isRequired,
+    topLevelClasses: PropTypes.array,
+    messageBody: PropTypes.any.isRequired,
+    repeat: PropTypes.any,
+    frame: PropTypes.any,
+    attachment: PropTypes.any,
+    stacktrace: PropTypes.any,
+    messageId: PropTypes.string,
+    scrollToMessage: PropTypes.bool,
+    onViewSourceInDebugger: PropTypes.func,
+    sourceMapService: PropTypes.any,
+  },
 
-Message.defaultProps = {
-  topLevelClasses: [],
-};
+  getDefaultProps() {
+    return {
+      topLevelClasses: [],
+    };
+  },
 
-function Message(props) {
-  const {
-    messageId,
-    open,
-    source,
-    type,
-    level,
-    topLevelClasses,
-    messageBody,
-    frame,
-    stacktrace,
-    onViewSourceInDebugger,
-    sourceMapService,
-    dispatch,
-  } = props;
+  componentDidMount() {
+    if (this.props.scrollToMessage && this.messageNode) {
+      this.messageNode.scrollIntoView();
+    }
+  },
 
-  topLevelClasses.push("message", source, type, level);
-  if (open) {
-    topLevelClasses.push("open");
-  }
+  render() {
+    const {
+      messageId,
+      open,
+      source,
+      type,
+      level,
+      topLevelClasses,
+      messageBody,
+      frame,
+      stacktrace,
+      onViewSourceInDebugger,
+      sourceMapService,
+      dispatch,
+    } = this.props;
 
-  const icon = MessageIcon({level});
+    topLevelClasses.push("message", source, type, level);
+    if (open) {
+      topLevelClasses.push("open");
+    }
+
+    const icon = MessageIcon({level});
 
-  // Figure out if there is an expandable part to the message.
-  let attachment = null;
-  if (props.attachment) {
-    attachment = props.attachment;
-  } else if (stacktrace) {
-    const child = open ? StackTrace({
-      stacktrace: stacktrace,
-      onViewSourceInDebugger: onViewSourceInDebugger
-    }) : null;
-    attachment = dom.div({ className: "stacktrace devtools-monospace" }, child);
-  }
+    // 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) {
+      const child = open ? StackTrace({
+        stacktrace: stacktrace,
+        onViewSourceInDebugger: onViewSourceInDebugger
+      }) : null;
+      attachment = dom.div({ className: "stacktrace devtools-monospace" }, child);
+    }
 
-  // If there is an expandable part, make it collapsible.
-  let collapse = null;
-  if (attachment) {
-    collapse = CollapseButton({
-      open,
-      onClick: function () {
-        if (open) {
-          dispatch(actions.messageClose(messageId));
-        } else {
-          dispatch(actions.messageOpen(messageId));
-        }
-      },
-    });
-  }
+    // If there is an expandable part, make it collapsible.
+    let collapse = null;
+    if (attachment) {
+      collapse = CollapseButton({
+        open,
+        onClick: function () {
+          if (open) {
+            dispatch(actions.messageClose(messageId));
+          } else {
+            dispatch(actions.messageOpen(messageId));
+          }
+        },
+      });
+    }
 
-  const repeat = props.repeat ? MessageRepeat({repeat: props.repeat}) : null;
+    const repeat = this.props.repeat ? MessageRepeat({repeat: this.props.repeat}) : null;
 
-  // Configure the location.
-  const shouldRenderFrame = frame && frame.source !== "debugger eval code";
-  const location = dom.span({ className: "message-location devtools-monospace" },
-    shouldRenderFrame ? FrameView({
-      frame,
-      onClick: onViewSourceInDebugger,
-      showEmptyPathAsHost: true,
-      sourceMapService
-    }) : null
-  );
+    // Configure the location.
+    const shouldRenderFrame = frame && frame.source !== "debugger eval code";
+    const location = dom.span({ className: "message-location devtools-monospace" },
+      shouldRenderFrame ? FrameView({
+        frame,
+        onClick: onViewSourceInDebugger,
+        showEmptyPathAsHost: true,
+        sourceMapService
+      }) : null
+    );
 
-  return dom.div({ className: topLevelClasses.join(" ") },
-    // @TODO add timestamp
-    // @TODO add indent if necessary
-    icon,
-    collapse,
-    dom.span({ className: "message-body-wrapper" },
-      dom.span({ className: "message-flex-body" },
-        dom.span({ className: "message-body devtools-monospace" },
-          messageBody
+    return dom.div({
+      className: topLevelClasses.join(" "),
+      ref: node => {
+        this.messageNode = node;
+      }
+    },
+      // @TODO add timestamp
+      // @TODO add indent if necessary
+      icon,
+      collapse,
+      dom.span({ className: "message-body-wrapper" },
+        dom.span({ className: "message-flex-body" },
+          dom.span({ className: "message-body devtools-monospace" },
+            messageBody
+          ),
+          repeat,
+          location
         ),
-        repeat,
-        location
-      ),
-      attachment
-    )
-  );
-}
+        attachment
+      )
+    );
+  }
+});
 
 module.exports = Message;
--- a/devtools/client/webconsole/new-console-output/reducers/ui.js
+++ b/devtools/client/webconsole/new-console-output/reducers/ui.js
@@ -1,26 +1,37 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 constants = require("devtools/client/webconsole/new-console-output/constants");
+const {
+  FILTER_BAR_TOGGLE,
+  MESSAGE_ADD,
+} = require("devtools/client/webconsole/new-console-output/constants");
 const Immutable = require("devtools/client/shared/vendor/immutable");
 
 const UiState = Immutable.Record({
   filterBarVisible: false,
   filteredMessageVisible: false,
+  autoscroll: true,
 });
 
 function ui(state = new UiState(), action) {
+  // Autoscroll should be set for all action types. If the last action was not message
+  // add, then turn it off. This prevents us from scrolling after someone toggles a
+  // filter, or to the bottom of the attachement when an expandable message at the bottom
+  // of the list is expanded. It does depend on the MESSAGE_ADD action being the last in
+  // its batch, though.
+  state = state.set("autoscroll", action.type == MESSAGE_ADD);
+
   switch (action.type) {
-    case constants.FILTER_BAR_TOGGLE:
+    case FILTER_BAR_TOGGLE:
       return state.set("filterBarVisible", !state.filterBarVisible);
   }
 
   return state;
 }
 
 module.exports = {
   UiState,
--- a/devtools/client/webconsole/new-console-output/selectors/ui.js
+++ b/devtools/client/webconsole/new-console-output/selectors/ui.js
@@ -1,12 +1,20 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
 function getAllUi(state) {
   return state.ui;
 }
 
-exports.getAllUi = getAllUi;
+function getScrollSetting(state) {
+  return getAllUi(state).autoscroll;
+}
+
+module.exports = {
+  getAllUi,
+  getScrollSetting,
+};