Skip to content
calc.html 25.3 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 {
	  padding: 20px;
      }
      #help-overlay-content h4 {
      }
      #close-help-overlay {
	  position: relative;
	  left: 350px;
      }
      .button-group {
	  width: 100%;
	  display: flex;
	  justify-content: 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 {
	  width: 300px;
      }
      #help-overlay-content {
      }
      #close-help-overlay {
	  position: relative;
	  left: 100px;
      }
    </style>
  </head>

  <body>
    <div id="container">
      <div id="stack">
Yuanle Song's avatar
Yuanle Song committed
	<p>Stack <a title="clear stack" id="clear-stack" href="#">Clear Stack</a> <a title="expand stack" id="expand-stack" href="#">Expand Stack</a></p>
	<div id="stack-content"></div>
      </div>
      <div id="keyboard">
	<p>Keyboard
	  <a title="reset RPN calculator" id="reset" href="#">Reset All</a>
	  <a title="help (?)" id="help-link" href="#">Help</a>
	<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="minus" data-bt-name="minus">-</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="push pending number to stack or duplicate top number on stack (Enter)"
		  data-bt-name="return"></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>
	  </table>
	</div>
      </div>
      <div id="trail">
Yuanle Song's avatar
Yuanle Song committed
	<p>Trail <a title="clear trail" id="clear-trail" href="#">Clear Trail</a> <a title="expand trail" id="expand-trail" href="#">Expand Trail</a></p>
	<div id="trail-content"></div>
      </div>
    </div>
    <div id="help-overlay-container" class="overlay">
      <div id="help-overlay-content">
	<h3>HELP <a id="close-help-overlay" class="close-help-overlay" href="#">Close</a></h3>
	<h4>About RPN calculator</h4>
	<p>RPN calculator is a web implementation of a <a href="#TODO">reverse polish notation calculator</a>. You push numbers to stack first, then push operator keys which will pop number from stack and run the computation. 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="#TODO">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>
	<p>RPN calculator v1.0.0, Copyright (C) 2017  Yuanle Song &lt;sylecn@gmail.com&gt;</p>
	<div class="button-group">
	  <button class="close-help-overlay">Close</button>
	</div>
      </div>
    </div>

    <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 () {
	  const testing = false;
	  const uiTesting = false;
	  const enableServiceWorker = false;

	  // service worker
	  if ('serviceWorker' in navigator && enableServiceWorker) {
	      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) {
	      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");
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 () {
	      var 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);
	  };
	  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 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 () {
	      var 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 === "");

	      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 () {
	      var m = new fsm.RPNCalculator();
	      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();
	      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) {
Yuanle Song's avatar
Yuanle Song committed
	      var trailOp, trailNum, trNode;

	      $('#number-display').text(rpnCalculator.currentNumber);
	      $('#error-msg').text(rpnCalculator.lastError);
	      const stack = rpnCalculator.numberStack;
	      const stackSize = stack.length;
	      $("#stack-content").children().remove();
	      for (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 (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);
	      }
	      $("#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);
		      $('#help-overlay-container').hide();
		  });
	      });
	  });
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 Stack");
	      } else {
		  $('#stack-content').css("max-height", "100px");
		  $('#expand-stack').text("Expand Stack");
	      }
	      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 Trail");
	      } else {
		  $('#trail-content').css("max-height", "300px");
		  $('#expand-trail').text("Shrink Trail");
	      }
	      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",
		  // 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
	      }
	  });
      }());
    </script>
  </body>

</html>