Skip to content
calc.html 27.5 KiB
Newer Older
<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>RPN calculator</title>
    <style type="text/css">
      #container {
	  display: flex;
	  flex-flow: row;
	  width: 700px;
	  margin: 10px auto;
Yuanle Song's avatar
Yuanle Song committed
	  border: 1px skyblue solid;
      }
      #container > div {
	  /* border: 1px grey dotted; */
      }
      #container > div > p {
Yuanle Song's avatar
Yuanle Song committed
	  margin: 10px;
      }
      #stack {
	  width: 200px;
Yuanle Song's avatar
Yuanle Song committed
      #stack-content {
	  margin-left: 10px;
	  font-size: 1.2em;
	  height: 400px;
	  overflow: auto;
      }
      #expand-stack {
	  display: none;
      }
      #keyboard {
	  width: 300px;
	  height: 100%;
	  border-left: 1px skyblue dotted;
	  border-right: 1px skyblue dotted;
      }
      #trail {
	  width: 200px;
      }
      #trail-content {
	  margin-left: 10px;
	  font-size: 1.2em;
Yuanle Song's avatar
Yuanle Song committed
	  font-family: monospace;
	  height: 400px;
	  overflow: auto;
Yuanle Song's avatar
Yuanle Song committed
      #expand-trail {
	  display: none;
      }
      .keyboard {
	  font-size: 1.2em;
	  text-align: center;
	  padding-bottom: 10px;
      }
      .keyboard table {
	  margin: 2px auto;
Yuanle Song's avatar
Yuanle Song committed
	  border-collapse: collapse;
      }
      .keyboard td {
	  width: 60px;
	  height: 60px;
Yuanle Song's avatar
Yuanle Song committed
	  border: 1px skyblue solid;
      }
      #number-display {
Yuanle Song's avatar
Yuanle Song committed
	  font-family: monospace;
	  border: 1px skyblue solid;
	  width: 100%;
Yuanle Song's avatar
Yuanle Song committed
	  max-width: 150px;	/*just set to something small and it works in chrome.*/
	  overflow: auto;
	  text-align: right;
Yuanle Song's avatar
Yuanle Song committed
	  padding: 0 10px;
      }
      #error-msg {
	  height: 30px;
Yuanle Song's avatar
Yuanle Song committed
	  border-style: none;
Yuanle Song's avatar
Yuanle Song committed
      .trail-op {
	  min-width: 40px;
Yuanle Song's avatar
Yuanle Song committed
	  text-align: right;
      }
      .trail-num {
	  padding-left: 10px;
Yuanle Song's avatar
Yuanle Song committed
	  min-width: 10px;
	  text-align: left;
      }
      .trail-active td.trail-op {
	  border-left: 2px lightblue solid;
      }
      .overlay {
	  display: none;
      }
      #help-overlay-container {
	  width: 700px;
      }
      #help-overlay-content {
Yuanle Song's avatar
Yuanle Song committed
	  font-family: sans;
      }
      #help-overlay-content h4 {
      }
      .button-group {
	  width: 100%;
	  display: flex;
	  justify-content: center;
      }
Yuanle Song's avatar
Yuanle Song committed

      /* title with buttons on the right. see ./test-rhs-link-on-title-row.html */
      .title {
	  display: flex;
	  /* border: 1px black solid; */
      }
      .title h3 {
	  margin: 8px 16px;
	  font-size: 1em;
      }
      .title-button-group {
	  flex-grow: 1;
	  display: flex;
	  justify-content: flex-end;
	  margin: 8px 1em;
      }
      .title-button-group a, .title-button-group button {
	  padding: 0 5px;
      }
      #help-title {
	  text-align: center;
      }
      #copyright {
	  text-align: center;
      }
Yuanle Song's avatar
Yuanle Song committed
@media only screen and (max-device-width: 480px) {
      #container {
	  width: 300px;
	  flex-flow: column;
      }
      #stack {
	  height: 100%;
	  width: 300px;
	  order: 200;
      }
      #stack-content {
	  height: 100%;
	  max-height: 100px;
      }
      #expand-stack {
	  display: inline;
      }
      #keyboard {
	  height: 100%;
	  order: 100;
      }
      #trail {
	  height: 100%;
	  width: 300px;
	  order: 300;
      }
      #trail-content {
	  height: 100%;
	  max-height: 100px;
      }
      #expand-trail {
	  display: inline;
      }
      #error-msg {
	  height: 20px;
      }
      .keyboard td {
	  width: 60px;
	  height: 50px;
      }
      #help-overlay-container {
Yuanle Song's avatar
Yuanle Song committed
	  width: 100%;
	  margin: 0px auto;
      }
      #help-overlay-content {
Yuanle Song's avatar
Yuanle Song committed
	  padding: 5px;
    </style>
  </head>

  <body>
    <div id="container">
      <div id="stack">
Yuanle Song's avatar
Yuanle Song committed
	<div class="title">
	  <h3>Stack</h3>
	  <div class="title-button-group">
	    <a title="clear stack" id="clear-stack" href="#">Clear</a>
	    <a title="expand stack" id="expand-stack" href="#">Expand</a>
Yuanle Song's avatar
Yuanle Song committed
	  </div>
	</div>
	<div id="stack-content"></div>
      </div>
      <div id="keyboard">
Yuanle Song's avatar
Yuanle Song committed
	<div class="title">
	  <h3>Keyboard</h3>
	  <div class="title-button-group">
	    <a title="reset RPN calculator" id="reset" href="#">Reset All</a>
	    <a title="help (?)" id="help-link" href="#">Help</a>
	  </div>
	</div>
	<div class="keyboard">
	  <table>
	    <tr>
	      <td colspan="4" id="error-msg"></td>
	    </tr>
	    <tr>
	      <td colspan="4" id="number-display"></td>
	      <td data-bt-name="num7">7</td>
	      <td data-bt-name="num8">8</td>
	      <td data-bt-name="num9">9</td>
	      <td title="delete pending digit or last number in stack (q)" data-bt-name="backspace"></td>
	      <td data-bt-name="num4">4</td>
	      <td data-bt-name="num5">5</td>
	      <td data-bt-name="num6">6</td>
	      <td title="times (*)" data-bt-name="times">×</td>
	      <td data-bt-name="num1">1</td>
	      <td data-bt-name="num2">2</td>
	      <td data-bt-name="num3">3</td>
	      <td title="push pending number to stack or duplicate top number on stack (Enter)"
		  data-bt-name="return"></td>
	      <td data-bt-name="num0">0</td>
Yuanle Song's avatar
Yuanle Song committed
	      <td title="change sign (n)" data-bt-name="change-sign">±</td>
	      <td title="dot" data-bt-name="dot">.</td>
	      <td title="plus" data-bt-name="plus">+</td>
	      <td title="swap top two numbers on stack" data-bt-name="swap">SWAP</td>
Yuanle Song's avatar
Yuanle Song committed
	      <td title="undo last operator (U)" data-bt-name="undo">UNDO</td>
	      <td title="divide (/)" data-bt-name="divide">÷</td>
	      <td title="minus" data-bt-name="minus">-</td>
	    <tr>
	      <td title="sum all numbers in stack" data-bt-name="sum-all">Σ</td>
Yuanle Song's avatar
Yuanle Song committed
	      <td title="square root (Q)" data-bt-name="square-root"></td>
	      <td title="power (^)" data-bt-name="power">^</td>
Yuanle Song's avatar
Yuanle Song committed
	      <td title="round (R)" data-bt-name="round">ROND</td>
	  </table>
	</div>
      </div>
      <div id="trail">
Yuanle Song's avatar
Yuanle Song committed
	<div class="title">
	  <h3>Trail</h3>
	  <div class="title-button-group">
	    <a title="clear trail" id="clear-trail" href="#">Clear</a>
	    <a title="expand trail" id="expand-trail" href="#">Expand</a>
Yuanle Song's avatar
Yuanle Song committed
	  </div>
	</div>
	<div id="trail-content"></div>
      </div>
    </div>
    <div id="help-overlay-container" class="overlay">
      <div id="help-overlay-content">
Yuanle Song's avatar
Yuanle Song committed
	<h3 id="help-title" style="text-align: center">RPN calculator v1.1.0</h3>
Yuanle Song's avatar
Yuanle Song committed
	<h4>About</h4>
	<p>RPN calculator is a web implementation of a <a href="https://en.wikipedia.org/wiki/Reverse_Polish_notation">reverse polish notation calculator</a>. You push numbers to stack first, then press operator keys which will pop number from stack and run the calculation. For example, you may press num1, return, num2, num3, return, plus, and you will see result 24 in top of stack. Number is pushed to stack using return key.</p>
	<p>Supported operators are: +, -, *, /, square-root, power, sum-all. Utility features include delete number, duplicate number, swap top 2 number on stack, undo, clear stack. Hover over button to see what that button does and the hotkey for that button.</p>
	<p>This program should work on desktop and mobile browser. Tested in chrome and firefox.</p>
	<h4>Credit</h4>
	<p>This project is inspired by <a href="https://www.gnu.org/software/emacs/manual/html_mono/calc.html">emacs calc mode</a>. It tries to mimic the behavior when possible.</p>
	<h4>Debug Features</h4>
	<p>This website use service worker to make it work offline. If the local cache does not work for some reason, you may <a title="refresh page" id="refresh-page" href="#">force refresh page</a> to get latest version. You may also <a title="purge service worker cache" id="purge-service-worker-cache" href="#">purge all service worker cache</a>.</p>
Yuanle Song's avatar
Yuanle Song committed
	<p id="copyright">Copyright (C) 2017  Yuanle Song &lt;sylecn@gmail.com&gt;</p>
	<div class="button-group">
Yuanle Song's avatar
Yuanle Song committed
	  <button class="close-help-overlay">Return to Calculator</button>
    <script type="text/javascript" src="vendor/localforage.min.js"></script>
    <script type="text/javascript" src="vendor/jquery.min.js"></script>
    <script type="text/javascript" src="fifo.js"></script>
    <script type="text/javascript" src="fsm.js"></script> <!-- fifo.js is required by fsm.js -->
    <script type="text/javascript">
      (function () {
	  'use strict';
	  const testing = false;
	  const uiTesting = false;
Yuanle Song's avatar
Yuanle Song committed
	  const aboutPageTesting = false;
Yuanle Song's avatar
Yuanle Song committed
	  const enableServiceWorker = true;

	  // service worker
	  if ('serviceWorker' in navigator && enableServiceWorker && window.location.protocol !== "file:") {
	      navigator.serviceWorker
		  .register('sw.js', {scope: './'})
		  .then(function() {
		      console.log("Service Worker Registered");
		  });
	  }

	  /**
	   * show dumb data for UI testing.
	   */
Yuanle Song's avatar
Yuanle Song committed
	  const showDumbData = function (rpnCalculator) {
	      rpnCalculator.sendKey("num1");
	      rpnCalculator.sendKey("return");
	      rpnCalculator.sendKey("num2");
	      rpnCalculator.sendKey("return");
	      rpnCalculator.sendKey("num3");
	      rpnCalculator.sendKey("return");
	      for (let i = 0; i < 30; ++i) {
Yuanle Song's avatar
Yuanle Song committed
		  rpnCalculator.sendKey("return");
		  rpnCalculator.sendKey("plus");
	      }
	      for (let i = 0; i < 30; ++i) {
Yuanle Song's avatar
Yuanle Song committed
		  rpnCalculator.sendKey("return");
	      }

	      rpnCalculator.sendKey("num7");
	      rpnCalculator.sendKey("num8");
	      rpnCalculator.sendKey("num9");
Yuanle Song's avatar
Yuanle Song committed
	      // for (i = 0; i < 10; ++i) {
	      // 	  rpnCalculator.sendKey("num" + i);
	      // 	  rpnCalculator.sendKey("num" + i);
	      // }
Yuanle Song's avatar
Yuanle Song committed
	      // $("#number-display").text('789');
Yuanle Song's avatar
Yuanle Song committed
	      // $("#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 () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 2);
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);
Yuanle Song's avatar
Yuanle Song committed

	      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 () {
	      let m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 12);
	      console.assert(m.currentNumber === "");

	      m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num0");
	      m.sendKey("dot");
	      m.sendKey("num1");
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 0.12);
	      console.assert(m.currentNumber === "");

	      m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num0");
	      m.sendKey("dot");
	      m.sendKey("num1");
	      m.sendKey("num2");
	      m.sendKey("backspace");
	      console.assert(m.currentNumber === "0.1");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 0.1);
	      console.assert(m.currentNumber === "");
	  };
	  const rpnTestAutoCommitNumber = function () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num2");
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);
	      console.assert(m.currentNumber === "");
	      console.assert(m.currentState === "idle");
	  };
	  const rpnTestOperator = function () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 2);
	      m.sendKey("minus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === -1);
	      m.sendKey("num3");
	      m.sendKey("num0");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 2);
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 29);
	      m.sendKey("return");    // duplicate
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 29 * 2);
	      m.sendKey("num9");
	      m.sendKey("change-sign");
	      m.sendKey("swap");
	      console.assert(m.numberStack.length === 2);
	      console.assert(m.numberStack[0] === -9);
	      console.assert(m.numberStack[1] === 29 * 2);
Yuanle Song's avatar
Yuanle Song committed

	      m.sendKey("backspace");
	      m.sendKey("backspace");    // no number on stack now.

	      // test round, sqrt, power
	      m.sendKey("num1");
	      m.sendKey("dot");
	      m.sendKey("num2");
	      m.sendKey("return");
	      m.sendKey("round");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 1);

	      m.sendKey("num9");
	      m.sendKey("times");
	      m.sendKey("square-root");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);

	      m.sendKey("num2");
	      m.sendKey("power");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 9);
	  const rpnTestBackspaceOperator = function () {
	      const m = new fsm.RPNCalculator();
	      m.sendKey("num1");
	      m.sendKey("num2");
	      console.assert(m.currentNumber === "12");
	      m.sendKey("backspace");    // remove last digit in current number.
	      console.assert(m.currentNumber === "1");
	      m.sendKey("num3");
	      m.sendKey("return");
	      console.assert(m.currentNumber === "");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 13);

	      m.sendKey("backspace");    // delete last number in stack.
	      console.assert(m.numberStack.length === 0);
	  };
	  const rpnTestBackspaceOperatorDeleteAllDigits = function () {
	      const m = new fsm.RPNCalculator();
	      m.sendKey("num1");
	      console.assert(m.currentNumber === "1");
	      m.sendKey("backspace");    // remove last digit in current number.
	      console.assert(m.currentNumber === "0");
	      m.sendKey("num3");
	      m.sendKey("return");
	      console.assert(m.currentNumber === "");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);

	      m.sendKey("backspace");    // delete last number in stack.
	      console.assert(m.numberStack.length === 0);
	  };
	  const rpnTestSumAllOperator = function () {
	      const m = new fsm.RPNCalculator();
	      m.sendKey("sum-all");
	      console.assert(m.lastError !== null);
	      m.sendKey("num1");
	      m.sendKey("sum-all");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 1);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num2");
	      m.sendKey("return");
	      m.sendKey("num3");
	      m.sendKey("return");
	      m.sendKey("num1");
	      m.sendKey("change-sign");
	      m.sendKey("return");
	      m.sendKey("sum-all");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 5);
	  };
	  const rpnTestNumberErrorHandling = function () {
	      let m = new fsm.RPNCalculator();
	      m.sendKey("num0");
	      m.sendKey("num0");
	      console.assert(m.currentNumber === "0");

	      m = new fsm.RPNCalculator();
	      m.sendKey("dot");
	      console.assert(m.currentNumber === "0.");
	      m.sendKey("backspace");
	      console.assert(m.currentNumber === "0");
	      m.sendKey("backspace");
	      console.assert(m.currentNumber === "0");

	      m = new fsm.RPNCalculator();
	      m.sendKey("num1");
	      m.sendKey("dot");
	      m.sendKey("num2");
	      console.assert(m.currentNumber === "1.2");
	      m.sendKey("dot");    // future dot is ignored and show error.
	      console.assert(m.currentNumber === "1.2");
	      m.sendKey("num3");
	      console.assert(m.currentNumber === "1.23");
	      m.sendKey("dot");
	      console.assert(m.currentNumber === "1.23");
	      m.sendKey("return");
	      console.assert(m.numberStack[0] === 1.23);
	  };
	  const rpnTestNotEnoughElementOnStack = function () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 1);
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 1);
	      console.assert(m.lastError !== null);
	      m.sendKey("backspace");
	      console.assert(m.numberStack.length === 0);
	      console.assert(m.lastError === null);
	      m.sendKey("change-sign");
	      console.assert(m.lastError !== null);
	      m.sendKey("num1");
	      console.assert(m.lastError === null);
	      m.sendKey("change-sign");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === -1);
	      console.assert(m.lastError === null);
	  };
Yuanle Song's avatar
Yuanle Song committed
	  const rpnTestUndo = function () {
	      const m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      console.assert(m.snapshots.data.length === 1);
	      console.assert(m.snapshots.data[0].numberStack.length === 0);
	      m.sendKey("num2");
	      m.sendKey("return");
	      console.assert(m.snapshots.data.length === 2);
	      console.assert(m.snapshots.data[0].numberStack.length === 0);
	      console.assert(m.snapshots.data[1].numberStack.length === 1);
	      console.assert(m.snapshots.data[1].numberStack[0] === 1);

	      console.assert(m.numberStack.length === 2);
	      console.assert(m.numberStack[0] === 1);
	      console.assert(m.numberStack[1] === 2);

	      m.sendKey("undo");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 1);
	      m.sendKey("num3");
	      m.sendKey("return");
	      m.sendKey("plus");
	      m.sendKey("undo");
	      m.sendKey("times");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === 3);
	      m.sendKey("undo");
	      m.sendKey("minus");
	      console.assert(m.numberStack.length === 1);
	      console.assert(m.numberStack[0] === -2);
	  };
	  const rpnTestUndoWithErrors = function () {
	      let m = new fsm.RPNCalculator();
Yuanle Song's avatar
Yuanle Song committed
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("plus");
	      console.assert(m.numberStack.length === 1);
	      m.sendKey("undo");
	      console.assert(m.numberStack.length === 0);

	      m = new fsm.RPNCalculator();
	      console.assert(m.numberStack.length === 0);
	      m.sendKey("num8");
	      m.sendKey("return");
	      m.sendKey("num1");
	      m.sendKey("return");
	      m.sendKey("num0");
	      m.sendKey("divide");
	      m.sendKey("undo");
	      console.assert(m.numberStack.length === 3);
	      console.assert(m.numberStack[0] === 8);
	      console.assert(m.numberStack[1] === 1);
	      console.assert(m.numberStack[2] === 0);
	      m.sendKey("undo");
	      console.assert(m.numberStack.length === 2);
	      console.assert(m.numberStack[0] === 8);
	      console.assert(m.numberStack[1] === 1);
	  };

	  /**
	   * run all tests.
	   */
	  const runTests = function () {
	      console.assert(fsm.matchesSubstringAbb("123abb123") === true);
	      console.assert(fsm.matchesSubstringAbb("123ab123") === false);
	      console.assert(fsm.matchesSubstringAbb("123abaabb123") === true);
	      console.assert(fsm.matchesSubstringAbb("123abbb123") === true);
	      console.assert(fsm.matchesSubstringAbb("123ababab123") === false);
	      console.assert(fsm.matchesSubstringAbb("") === false);
	      console.assert(fsm.matchesSubstringAbb("123") === false);
	      console.assert(fsm.matchesSubstringAbb("123ab") === false);
	      console.assert(fsm.matchesSubstringAbb("123abb") === true);
	      console.assert(fsm.matchesSubstringAbb("abb") === true);

	      rpnTestBasic();
	      rpnTestNumberHandling();
	      rpnTestAutoCommitNumber();
	      rpnTestOperator();

	      rpnTestNumberErrorHandling();
	      rpnTestNotEnoughElementOnStack();
	      rpnTestBackspaceOperator();
              rpnTestBackspaceOperatorDeleteAllDigits();
	      rpnTestSumAllOperator();
Yuanle Song's avatar
Yuanle Song committed

	      rpnTestUndo();
	      rpnTestUndoWithErrors();
	  /**
	   * update web UI to match the state of th rpnCalculator.
	   *
	   * currently it syncs rpnCalculator.currentNumber and rpnCalculator.numberStack.
	   */
	  const updateUI = function (rpnCalculator) {
	      let trailOp, trailNum, trNode;
Yuanle Song's avatar
Yuanle Song committed

	      $('#number-display').text(rpnCalculator.currentNumber);
	      $('#error-msg').text(rpnCalculator.lastError);
	      const stack = rpnCalculator.numberStack;
	      const stackSize = stack.length;
	      $("#stack-content").children().remove();
	      for (let i = 0; i < stackSize; ++i) {
		  $("#stack-content").append($('<pre>').text((stackSize - i) + ': ' + stack[i]));
	      }
Yuanle Song's avatar
Yuanle Song committed
	      $("#stack-content").append($('<pre>').text('   .')).scrollTop(9000); // TODO how to scroll to bottom?
Yuanle Song's avatar
Yuanle Song committed

	      const trail = rpnCalculator.trail.data;
	      const trailSize = trail.length;
	      const trailTable = $('<table>');
	      for (let i = 0; i < trailSize; ++i) {
Yuanle Song's avatar
Yuanle Song committed
		  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);
	      }
	      $("#trail-content").children().remove();
Yuanle Song's avatar
Yuanle Song committed
	      if (trailSize > 0) {
Yuanle Song's avatar
Yuanle Song committed
		  $("#trail-content").append(trailTable).scrollTop(9000); // TODO how to scroll to bottom?
Yuanle Song's avatar
Yuanle Song committed
	      }
	  // page init
	  if (testing) {
	      runTests();
	  }
	  const rpnCalculator = new fsm.RPNCalculator();

	  if (uiTesting) {
	      showDumbData(rpnCalculator);
	  }
	  // init web UI
	  updateUI(rpnCalculator);

	  // setup event handler
	  $('.keyboard td').click(function (evt) {
	      const target = evt.target;
	      const keyName = $(target).attr('data-bt-name');
	      if (keyName !== undefined) {
		  console.log("user clicked on button: " + keyName);
		  rpnCalculator.sendKey(keyName);
		  // update UI
		  updateUI(rpnCalculator);
	      }
	  $('#clear-trail').click(function (evt) {
	      rpnCalculator.clearTrail();
	      updateUI(rpnCalculator);
	      return false;
	  });
	  $('#clear-stack').click(function (evt) {
	      rpnCalculator.clearStack();
	      updateUI(rpnCalculator);
	      return false;
	  });
	  $('#reset').click(function (evt) {
	      rpnCalculator.reset();
	      updateUI(rpnCalculator);
	      return false;
	  });
	  const showHelpOverlay = function () {
	      $('#container').hide();
	      $('#help-overlay-container').slideDown();
	  };
	  const hideHelpOverlay = function () {
	      $('#help-overlay-container').hide();
	      $('#container').show();
	  };
	  $('#help-link').click(function (evt) {
	      showHelpOverlay();
	      return false;
	  });
	  $('.close-help-overlay').click(function (evt) {
	      hideHelpOverlay();
	  $('#refresh-page').click(function (evt) {
	      const urlJoin = function (baseUrl, path) {
		  const lastSlash = baseUrl.lastIndexOf("/");
		  return baseUrl.substring(0, lastSlash + 1) + path;
	      };
	      console.assert(urlJoin("http://localhost:8000/calc.html", "fifo.js") ===
			     "http://localhost:8000/fifo.js");
	      console.assert(urlJoin("http://localhost:8000/", "fifo.js") ===
			     "http://localhost:8000/fifo.js");
	      console.assert(urlJoin("http://localhost:8000/abc/t1.html", "fifo.js") ===
			     "http://localhost:8000/abc/fifo.js");
	      console.assert(urlJoin("https://www.example.com/calc.html", "fifo.js") ===
			     "https://www.example.com/fifo.js");
	      console.assert(urlJoin("https://www.example.com/", "fifo.js") ===
			     "https://www.example.com/fifo.js");

	      // set service worker cache to expire, thus fetching latest page on next refresh.
	      const htmlUrl = window.location.href;
	      const fifoJsUrl = urlJoin(htmlUrl, 'fifo.js');
	      const fsmJsUrl = urlJoin(htmlUrl, 'fsm.js');
	      // clear cache in page load order. the basic dependence should
	      // be cleared first before trigger a page reload.
	      localforage.setItem('timestamp:' + fsmJsUrl, new Date(2016,0,1).toJSON()).then(function (_) {
		  localforage.setItem('timestamp:' + fifoJsUrl, new Date(2016,0,1).toJSON()).then(function (_) {
		      localforage.setItem('timestamp:' + htmlUrl, new Date(2016,0,1).toJSON()).then(function (_) {
			  window.location.href = window.location.href;
		      });
		  });
	      });
	      return false;
	  });
	  $('#purge-service-worker-cache').click(function (evt) {
	      localforage.clear().then(function (_) {
		  caches.delete('calc').then(function (_) {
		      const msg = "clear all done";
		      console.log(msg);
		      $('#error-msg').text(msg);
		      hideHelpOverlay();
	      return false;
Yuanle Song's avatar
Yuanle Song committed
	  $('#expand-stack').click(function (evt) {
	      const oldMaxHeight = $('#stack-content').css("max-height");
	      if (oldMaxHeight === "100px") {
		  $('#stack-content').css("max-height", "300px");
		  $('#expand-stack').text("Shrink");
Yuanle Song's avatar
Yuanle Song committed
	      } else {
		  $('#stack-content').css("max-height", "100px");
		  $('#expand-stack').text("Expand");
	      return false;
Yuanle Song's avatar
Yuanle Song committed
	  });
	  $('#expand-trail').click(function (evt) {
	      const oldMaxHeight = $('#trail-content').css("max-height");
	      if (oldMaxHeight === "300px") {
		  $('#trail-content').css("max-height", "100px");
		  $('#expand-trail').text("Expand");
Yuanle Song's avatar
Yuanle Song committed
	      } else {
		  $('#trail-content').css("max-height", "300px");
		  $('#expand-trail').text("Shrink");
	      return false;
Yuanle Song's avatar
Yuanle Song committed
	  });
Yuanle Song's avatar
Yuanle Song committed
	  $(document).keypress(function (evt) {
	      // map evt.key to RPNCalculator keyName.
	      const keyMap = {
		  "1": "num1",
		  "2": "num2",
		  "3": "num3",
		  "4": "num4",
		  "5": "num5",
		  "6": "num6",
		  "7": "num7",
		  "8": "num8",
		  "9": "num9",
		  "0": "num0",
		  ".": "dot",
		  "Enter": "return",
		  "+": "plus",
		  "-": "minus",
		  "*": "times",
		  "/": "divide",
		  "n": "change-sign",
		  "^": "power",
		  "Q": "square-root",
		  "U": "undo",
Yuanle Song's avatar
Yuanle Song committed
		  "R": "round",
Yuanle Song's avatar
Yuanle Song committed
		  // below is not from emacs calc
		  "q": "backspace",
	      };
	      console.log("charCode=" + evt.charCode + ", key=" + evt.key);
	      console.log(typeof(evt.key));
	      const keyName = keyMap[evt.key];
	      if (keyName !== undefined) {
		  rpnCalculator.sendKey(keyName);
		  updateUI(rpnCalculator);
	      } else {
		  if (evt.key === "?") {
		      const helpVisible = $('#help-overlay-container').css("display") === "block";
		      if (helpVisible) {
			  hideHelpOverlay();
		      } else {
			  showHelpOverlay();
		      }
Yuanle Song's avatar
Yuanle Song committed
	      }
	  });
Yuanle Song's avatar
Yuanle Song committed
	  if (aboutPageTesting) {
	      showHelpOverlay();
	  }
      }());
    </script>
  </body>

</html>