playground/client: Keep console scrolled to bottom unless user scrolls up.

Change-Id: Ifcc39656503ece0e713d51ce9764a0a43b3ca525
diff --git a/client/src/javascript/embedded.js b/client/src/javascript/embedded.js
index 1d57580..ce0bdb7 100644
--- a/client/src/javascript/embedded.js
+++ b/client/src/javascript/embedded.js
@@ -26,6 +26,9 @@
   this.editors_ = _.map(this.files_, function(file) {
     return new Editor(file.type, file.body);
   });
+  // scrollState_ changes should not trigger render_, thus are not monitored
+  // by mercury.
+  this.scrollState_ = {bottom: true};
   var state = m.state({
     activeTab: m.value(0),
     nextRunId: m.value(0),
@@ -39,6 +42,8 @@
     }
   });
   m.app(el, state, this.render_.bind(this));
+  // Enable ev-scroll listening.
+  m.Delegator().listenTo('scroll');
 }
 
 EmbeddedPlayground.prototype.renderTopBar_ = function(state) {
@@ -77,7 +82,7 @@
   return h('div.editors', editors);
 };
 
-function renderConsoleEvent(event) {
+EmbeddedPlayground.prototype.renderConsoleEvent_ = function(event) {
   var children = [];
   if (event.Timestamp) {
     // Convert UTC to local time.
@@ -96,12 +101,38 @@
   children.push(h('span.message.' + (event.Stream || 'unknown'),
                   event.Message));
   return h('div', children);
+};
+
+// ScrollHandle provides a hook to keep the console scrolled to the bottom
+// unless the user has scrolled up, and the update method to detect the
+// user scrolling up.
+function ScrollHandle(scrollState) {
+  this.scrollState_ = scrollState;
 }
 
+ScrollHandle.prototype.hook = function(elem, propname) {
+  var scrollState = this.scrollState_;
+  process.nextTick(function() {
+    if (scrollState.bottom) {
+      elem.scrollTop = elem.scrollHeight - elem.clientHeight;
+    }
+  });
+};
+
+ScrollHandle.prototype.update = function(ev) {
+  var elem = ev.target;
+  this.scrollState_.bottom =
+      elem.scrollTop === elem.scrollHeight - elem.clientHeight;
+};
+
 EmbeddedPlayground.prototype.renderConsole_ = function(state) {
   if (state.hasRun) {
-    return h('div.console.open', [
-      h('div.text', _.map(state.consoleEvents, renderConsoleEvent))
+    var scrollHandle = new ScrollHandle(this.scrollState_);
+    return h('div.console.open', {
+      'ev-scroll': scrollHandle.update.bind(scrollHandle),
+      'scrollhook': scrollHandle
+    }, [
+      h('div.text', _.map(state.consoleEvents, this.renderConsoleEvent_))
     ]);
   }
   return h('div.console');
@@ -132,6 +163,7 @@
   state.running.set(true);
   state.hasRun.set(true);
   state.consoleEvents.set([{Message: 'Running...'}]);
+  this.scrollState_.bottom = true;
 
   var myUrl = url.parse(window.location.href, true);
   var pgaddr = myUrl.query.pgaddr;
@@ -285,6 +317,7 @@
 // Clears the console and resets all editors to their original contents.
 EmbeddedPlayground.prototype.reset = function(state) {
   state.consoleEvents.set([]);
+  this.scrollState_.bottom = true;
   _.forEach(this.editors_, function(editor) {
     editor.reset();
   });