From 01b1b38b1a0015955f6bba2e55318d37bb316953 Mon Sep 17 00:00:00 2001
From: Yuanle Song <sylecn@gmail.com>
Date: Fri, 27 Jan 2017 16:34:01 +0800
Subject: [PATCH] make trait work.

show scrollbar on stack and trait when content overflows.
---
 calc.html   | 94 ++++++++++++++++++++++++++++++++++++++++++++---------
 fifo.js     |  6 ++--
 fsm.js      | 72 ++++++++++++++++++++++++++++++++--------
 operational | 33 +++++++++++++++++--
 4 files changed, 171 insertions(+), 34 deletions(-)

diff --git a/calc.html b/calc.html
index d307835..1db5d51 100644
--- a/calc.html
+++ b/calc.html
@@ -21,23 +21,28 @@
       }
       #stack {
 	  width: 200px;
-	  height: 500px
+	  height: 500px;
       }
       #keyboard {
 	  width: 300px;
-	  height: 500px
+	  height: 500px;
       }
       #trail {
 	  width: 200px;
-	  height: 500px
+	  height: 500px;
       }
       #stack-content {
 	  margin-left: 10px;
 	  font-size: 1.2em;
+	  height: 400px;
+	  overflow: auto;
       }
       #trail-content {
 	  margin-left: 10px;
 	  font-size: 1.2em;
+	  font-family: monospace;
+	  height: 400px;
+	  overflow: auto;
       }
       .keyboard {
 	  font-size: 1.2em;
@@ -59,6 +64,17 @@
       #error-msg {
 	  height: 30px;
       }
+      .trail-op {
+	  min-width: 10px;
+	  text-align: right;
+      }
+      .trail-num {
+	  min-width: 10px;
+	  text-align: left;
+      }
+      .trail-active td.trail-op {
+	  border-left: 2px lightblue solid;
+      }
     </style>
   </head>
 
@@ -124,17 +140,36 @@
 	  /**
 	   * show dumb data for UI testing.
 	   */
-	  const showDumbData = function () {
-	      $("#number-display").text('789');
+	  const showDumbData = function (rpnCalculator) {
+	      var i;
+	      rpnCalculator.sendKey("num1");
+	      rpnCalculator.sendKey("return");
+	      rpnCalculator.sendKey("num2");
+	      rpnCalculator.sendKey("return");
+	      rpnCalculator.sendKey("num3");
+	      rpnCalculator.sendKey("return");
+	      for (i = 0; i < 30; ++i) {
+		  rpnCalculator.sendKey("return");
+		  rpnCalculator.sendKey("plus");
+	      }
+	      for (i = 0; i < 30; ++i) {
+		  rpnCalculator.sendKey("return");
+	      }
+
+	      rpnCalculator.sendKey("num7");
+	      rpnCalculator.sendKey("num8");
+	      rpnCalculator.sendKey("num9");
 
-	      $("#stack-content").append($('<pre>').text('3: 1'));
-	      $("#stack-content").append($('<pre>').text('2: 2'));
-	      $("#stack-content").append($('<pre>').text('1: 3'));
-	      $("#stack-content").append($('<pre>').text('   .'));
+	      // $("#number-display").text('789');
 
-	      $("#trail-content").append($('<pre>').text('   1'));
-	      $("#trail-content").append($('<pre>').text('   2'));
-	      $("#trail-content").append($('<pre>').text('>  3'));
+	      // $("#stack-content").append($('<pre>').text('3: 1'));
+	      // $("#stack-content").append($('<pre>').text('2: 2'));
+	      // $("#stack-content").append($('<pre>').text('1: 3'));
+	      // $("#stack-content").append($('<pre>').text('   .'));
+
+	      // $("#trail-content").append($('<pre>').text('   1'));
+	      // $("#trail-content").append($('<pre>').text('   2'));
+	      // $("#trail-content").append($('<pre>').text('>  3'));
 	  };
 
 	  const rpnTestBasic = function () {
@@ -148,6 +183,15 @@
 	      m.sendKey("plus");
 	      console.assert(m.numberStack.length === 1);
 	      console.assert(m.numberStack[0] === 3);
+
+	      const trail = m.trail.data;    // the underlying array.
+	      console.assert(trail.length === 3);
+	      console.assert(trail[0][0] === null);
+	      console.assert(trail[0][1] === 1);
+	      console.assert(trail[1][0] === null);
+	      console.assert(trail[1][1] === 2);
+	      console.assert(trail[2][0] === '+');
+	      console.assert(trail[2][1] === 3);
 	  };
 	  const rpnTestNumberHandling = function () {
 	      var m = new fsm.RPNCalculator();
@@ -323,6 +367,8 @@
 	   * currently it syncs rpnCalculator.currentNumber and rpnCalculator.numberStack.
 	   */
 	  const updateUI = function (rpnCalculator) {
+	      var trailOp, trailNum, trNode;
+
 	      $('#number-display').text(rpnCalculator.currentNumber);
 	      $('#error-msg').text(rpnCalculator.lastError);
 	      const stack = rpnCalculator.numberStack;
@@ -331,16 +377,34 @@
 	      for (i = 0; i < stackSize; ++i) {
 		  $("#stack-content").append($('<pre>').text((stackSize - i) + ': ' + stack[i]));
 	      }
-	      $("#stack-content").append($('<pre>').text('   .'));
+	      $("#stack-content").append($('<pre>').text('   .')).scrollTop(900); // TODO how to scroll to bottom?
+
+	      const trail = rpnCalculator.trail.data;
+	      const trailSize = trail.length;
+	      const trailTable = $('<table>');
+	      for (i = 0; i < trailSize; ++i) {
+		  trailOp = trail[i][0];
+		  trailNum = trail[i][1];
+		  trNode = $('<tr>');
+		  if (i === trailSize - 1) {
+		      trNode.addClass("trail-active");
+		  }
+		  trNode.append($('<td>').text(trailOp).addClass("trail-op"));
+		  trNode.append($('<td>').text(trailNum).addClass("trail-num"));
+		  trailTable.append(trNode);
+	      }
+	      if (trailSize > 0) {
+		  $("#trail-content").children().remove();
+		  $("#trail-content").append(trailTable).scrollTop(900); // TODO how to scroll to bottom?
+	      }
 	  };
 
 	  // page init
-	  // showDumbData();
-
 	  runTests();
 
 	  const rpnCalculator = new fsm.RPNCalculator();
 
+	  // showDumbData(rpnCalculator);
 	  // init web UI
 	  updateUI(rpnCalculator);
 
diff --git a/fifo.js b/fifo.js
index e58237a..b8809d9 100644
--- a/fifo.js
+++ b/fifo.js
@@ -1,6 +1,7 @@
 var fifo = function () {
     /**
-     * an unbounded fifo queue.
+     * an unbounded fifo queue. Use push and pop to add or remove elements.
+     * you can also read the raw elements in this.data.
      */
     const Queue = function () {
 	this.data = [];
@@ -16,7 +17,8 @@ var fifo = function () {
     };
 
     /**
-     * a bounded fifo queue.
+     * a bounded fifo queue. Use push and pop to add or remove elements.
+     * you can also read the raw elements in this.data.
      */
     const BoundedQueue = function (capacity) {
 	this.capacity = capacity;
diff --git a/fsm.js b/fsm.js
index 82855f0..a7fc68f 100644
--- a/fsm.js
+++ b/fsm.js
@@ -78,7 +78,7 @@ var fsm = function () {
     	const stateIdle = "idle";
     	const stateWaitingForNumberOrAction = "waiting for number or action";
 
-	// keynames
+	// define constants for key name literals.
 	const keyNames = {
 	    num0: "num0",
 	    num1: "num1",
@@ -101,12 +101,37 @@ var fsm = function () {
 	    "return": "return",
 	    change_sign: "change-sign",
 	};
+	// map keyName to short key name.
+	const shortKeyNames = {
+	    "num0": "0",
+	    "num1": "1",
+	    "num2": "2",
+	    "num3": "3",
+	    "num4": "4",
+	    "num5": "5",
+	    "num6": "6",
+	    "num7": "7",
+	    "num8": "8",
+	    "num9": "9",
+	    "dot": ".",
+	    "backspace": "bksp",
+	    "times": "*",
+	    "divide": "/",
+	    "plus": "+",
+	    "minus": "-",
+	    "swap": "swap",
+	    "undo": "undo",
+	    "return": "ret",
+	    "change-sign": "chs",
+	};
 	const msgTooFewElementsOnStack = "Too few elements on stack";
+	const trailHistorySize = 50;
 
     	this.currentState = stateIdle;
     	this.numberStack = [];
     	this.currentNumber = "";
 	this.lastError = null;
+	this.trail = new fifo.BoundedQueue(trailHistorySize);
 
 	/**
 	 * save current state of this FSM. You can use load to restore the FSM state.
@@ -116,6 +141,7 @@ var fsm = function () {
 		"currentState": this.currentState,
 		"numberStack": this.numberStack,
 		"currentNumber": this.currentNumber,
+		"trail": this.trail,
 	    };
 	};
 	/**
@@ -125,6 +151,7 @@ var fsm = function () {
 	    this.currentState = data.currentState;
 	    this.numberStack = data.numberStack;
 	    this.currentNumber = data.currentNumber;
+	    this.trail = data.trail;
 	};
 	/**
 	 * set error msg. only the last error msg can be fetched via this.lastError.
@@ -149,7 +176,7 @@ var fsm = function () {
 	 * You may do early return if this function fails. this.lastError will
 	 * be set when false is returned.
 	 */
-	this.doBinaryOperator = function (func) {
+	this.doBinaryOperator = function (keyName, func) {
 	    const num2 = this.numberStack.pop();
 	    if (num2 === undefined) {
 		this.setErrorMsg(msgTooFewElementsOnStack);
@@ -171,6 +198,8 @@ var fsm = function () {
 		return false;
 	    }
 	    this.numberStack.push(result);
+
+	    this.trail.push([shortKeyName(keyName), result]);
 	    return true;
 	};
 
@@ -253,6 +282,13 @@ var fsm = function () {
 	    console.assert(r[0] === "1.2");
 	    console.assert(r[1] !== null);
 	};
+	/**
+	 * return short key name for given keyName.
+	 * this is used to show operator in shorter form in trails.
+	 */
+	const shortKeyName = function (keyName) {
+	    return shortKeyNames[keyName];
+	};
     	this.sendKey = function (keyName) {
 	    var num, num1, num2;
 	    var numString, errMsg;
@@ -273,31 +309,33 @@ var fsm = function () {
 		    this.currentState = stateWaitingForNumberOrAction;
 		} else {
 		    switch (keyName) {
-		    case keyNames.plus:
-			if (! this.doBinaryOperator((lhs, rhs) => lhs + rhs)) {
+		    case keyNames.change_sign:
+			num = this.numberStack.pop();
+			if (num === undefined) {
+			    this.setErrorMsg(msgTooFewElementsOnStack);
 			    return;
 			}
+		    	this.numberStack.push(-num);
+			console.log("trail push " + [keyName, shortKeyName(keyName), -num]);
+			this.trail.push([shortKeyName(keyName), -num]);
 		    	break;
-		    case keyNames.minus:
-			if (! this.doBinaryOperator((lhs, rhs) => lhs - rhs)) {
+		    case keyNames.plus:
+			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs + rhs)) {
 			    return;
 			}
 		    	break;
-		    case keyNames.change_sign:
-			num = this.numberStack.pop();
-			if (num === undefined) {
-			    this.setErrorMsg(msgTooFewElementsOnStack);
+		    case keyNames.minus:
+			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs - rhs)) {
 			    return;
 			}
-		    	this.numberStack.push(-num);
 		    	break;
 		    case keyNames.times:
-			if (! this.doBinaryOperator((lhs, rhs) => lhs * rhs)) {
+			if (! this.doBinaryOperator(keyName, (lhs, rhs) => lhs * rhs)) {
 			    return;
 			}
 		    	break;
 		    case keyNames.divide:
-			if (! this.doBinaryOperator((lhs, rhs) => {
+			if (! this.doBinaryOperator(keyName, (lhs, rhs) => {
 			    if (rhs === 0) {
 				this.setErrorMsg("Division by 0");
 				return null;
@@ -321,6 +359,7 @@ var fsm = function () {
 			}
 		    	this.numberStack.push(num2);
 		    	this.numberStack.push(num1);
+			// no changes on trail
 		    	break;
 		    case keyNames['return']:
 			// duplicate number
@@ -331,6 +370,7 @@ var fsm = function () {
 			}
 			this.numberStack.push(num);
 			this.numberStack.push(num);
+			// no changes on trail.
 		    	break;
 		    case keyNames.backspace:
 			// drop one number on stack
@@ -357,9 +397,13 @@ var fsm = function () {
 		    }
 		    // keep current state
 		} else if (keyName === keyNames['return']) {
-		    this.numberStack.push(parseFloat(this.currentNumber));
+		    // commit number to stack
+		    num = parseFloat(this.currentNumber);
+		    this.numberStack.push(num);
 		    this.currentNumber = "";
 		    this.currentState = stateIdle;
+
+		    this.trail.push([null, num]);
 		} else if (keyName === keyNames.backspace) {
 		    if (this.currentNumber.length > 0) {
 			this.currentNumber = this.currentNumber.substring(0, this.currentNumber.length - 1);
diff --git a/operational b/operational
index 7fb2db3..5e13b87 100644
--- a/operational
+++ b/operational
@@ -48,24 +48,43 @@ the parent DOM.
 
 * current                                                             :entry:
 ** 
+** 2017-01-27 add a reset button. browser refresh button is not always easily accessible, esp on mobile.
 ** 2017-01-27 make trail work
 - when is trail being updated?
   - when a number is committed to number stack. return.
   - when a new number is pushed to stack as an op result. plus, change-sign.
-  - swap doesn't change trail.
+  - dup and swap doesn't change trail.
+
 - this should be part of the FSM.
   what data should I store?
   it's a queue of [(Maybe op, number)].
 
   as mentioned previously, this data can be made persistent across
   pages/sessions.
-- first write a bounded fifo queue in javascript.
+
+- DONE first write a bounded fifo queue in javascript.
 - load persistent data for trails when init the FSM.
-- update the persistent data when trails has changed.
+  update the persistent data when trails has changed.
 
   // is updating local storage a heavy operation? I hope it doesn't sync to
   disk everytime I update a key.
 
+- implementation notes
+  - show last 10 entries in UI. allow user to use scrollbar to scroll to
+    earlier entries.
+
+    leave this UI optimization later. for now, just show last 10 or 20 entries.
+  - use table to show trail in HTML.
+    trailOp right align.
+    trailNum left align.
+    mark last entry using color highlight. there is no need for > marker.
+
+  - grep for "this.numberStack.push" to push elements to this.trail.
+
+  - change-sign op is not shown in trait. why?
+    it's because shortKeyNames key should be key name string literal, not the
+    js constant identifier.
+
 ** 2017-01-26 make trail persistent
 trail is a bounded fifo queue, it just store recent 1k entries.
 when multiple instances of calculator is working. they all write to a single
@@ -79,6 +98,14 @@ trail cache. it's not a problem.
 - DONE make DUP and SWP work
 - make trail work
 * done                                                                :entry:
+** 2017-01-27 stack, trail: overflow problem.
+- when there is not enough room, show scrollbar.
+- always show the title section.
+- auto scroll to bottom when content overflow.
+
+  TODO how to scroll to bottom?
+  $('#stack-content').scroll
+
 ** 2017-01-26 make < (backspace) work.
 ** 2017-01-26 make stack auto numbering work
 ** 2017-01-26 handle error for every this.numberStack.pop()
-- 
GitLab