diff --git a/calculator.appcache b/calculator.appcache index db21464..9b06826 100644 --- a/calculator.appcache +++ b/calculator.appcache @@ -1,5 +1,5 @@ CACHE MANIFEST -# v3.1.2: 2015-12-14 +# v3.2: 2015-12-17 index.html js/script.js css/style.css @@ -9,4 +9,4 @@ assets/touch-icon-iphone-120.png assets/touch-icon-iphone-152.png assets/touch-icon-iphone-180.png assets/touch-icon-iphone-76.png -assets/app-icon-large.png \ No newline at end of file +assets/app-icon-large.png diff --git a/css/style.less b/css/style.less index f5fbd7b..7de8af5 100644 --- a/css/style.less +++ b/css/style.less @@ -520,4 +520,4 @@ button { &:hover { color: #d9d9d9; } -} \ No newline at end of file +} diff --git a/index.html b/index.html index 5bf5dc3..d77e159 100644 --- a/index.html +++ b/index.html @@ -69,4 +69,4 @@ - \ No newline at end of file + diff --git a/js/script.js b/js/script.js index 8ebfff4..da000d5 100644 --- a/js/script.js +++ b/js/script.js @@ -4,23 +4,74 @@ * A calculator iOS web application that supports brackets, backspace and saved * calculation history. The app uses HTML5 app caching so it will work offline. * - * @version 3.1.2 + * @version 3.2 */ "use strict"; var devmode = true; +/** + * Returns the contents of the first item of an array + */ +if (!Array.prototype.first) { + Array.prototype.first = function() { + return this[0]; + } +} + + + +/** + * Returns the contents of the last item of an array + */ +if (!Array.prototype.last) { + Array.prototype.last = function() { + return this[this.length - 1]; + } +} + + + +/** + * Replace the contents of the last item in an array + * + * @param value string The new string for the last array item + */ +if (!Array.prototype.replaceLast) { + Array.prototype.replaceLast = function(value) { + this[this.length - 1] = value; + } +} + + + +/** + * Append a string to the string of the last item in an array + * + * @param value string The string to append + */ +if (!Array.prototype.appendToLast) { + Array.prototype.appendToLast = function(value) { + this[this.length - 1] += value; + } +} + + + +/** + * The Calcualtor constructor function + */ function Calculator() { this.settings = { - version: '3.1.2', - history: 100, + version: '3.2', + history: 50, fontsize: 60, decimals: 2 }; this.appstate = { - input: 0, + input: ['0'], brackets: 0, last: null }; @@ -44,7 +95,7 @@ function Calculator() { this.addEventHandlers(); // Restore previous app state - this.loadAppState(); + this.restoreAppState(); this.loadHistory(); this.updateDisplay(); @@ -53,9 +104,9 @@ function Calculator() { /** - * Retrieve the application state from local storage + * Retrieve and restore the application state from local storage */ -Calculator.prototype.loadAppState = function() { +Calculator.prototype.restoreAppState = function() { var json = localStorage.getItem('appState'), savedAppState; @@ -81,18 +132,149 @@ Calculator.prototype.saveAppState = function() { +/** + * Handles all events + */ +Calculator.prototype.addEventHandlers = function() { + var buttonModeStart = 'mousedown', + buttonModeEnd = 'mouseup'; + + if (window.navigator.hasOwnProperty('standalone') && window.navigator.standalone) { + buttonModeStart = 'touchstart'; + buttonModeEnd = 'touchend'; + } + + // Disable bounce scrolling on main application + document.getElementById('application').addEventListener(buttonModeStart, function(e) { + e.preventDefault(); + e.stopPropagation(); + }, false); + + // Fix bounce scrolling of whole page at top and bottom of content + document.getElementById('history-list-scroll').addEventListener('touchstart', function(e) { + var startTopScroll = this.scrollTop; + + if (document.getElementById('history-list').offsetHeight <= this.offsetHeight) { + e.preventDefault(); + e.stopPropagation(); + } + else { + if (startTopScroll <= 0) { + this.scrollTop = 1; + } + + if (startTopScroll + this.offsetHeight >= this.scrollHeight) { + this.scrollTop = this.scrollHeight - this.offsetHeight - 1; + } + } + }, false); + + // Keypad events + document.getElementById('btn-backspace').addEventListener(buttonModeStart, function() { + this.addTimer(this.backspaceLongPress.bind(this)); + }.bind(this), false); + + this.keypad.addEventListener(buttonModeEnd, function(event) { + if (!this.dragging) { + this.removeTimer(); + this.buttonEvent(event.target.value); + } + }.bind(this), false); + + // History list events + this.historyList.addEventListener(buttonModeStart, function() { + this.dragging = false; + }.bind(this), false); + + this.historyList.addEventListener('touchmove', function() { + this.dragging = true; + }.bind(this), false); + + this.historyList.addEventListener(buttonModeEnd, function(event) { + if (!this.dragging) { + this.appendHistoryItemToEquation(event.target.value); + this.closeHistoryPanel(); + } + }.bind(this), false); + + // History close events + this.historyClose.addEventListener(buttonModeStart, function() { + this.addTimer(this.clearHistory.bind(this)); + }.bind(this), false); + + this.historyClose.addEventListener(buttonModeEnd, function() { + this.removeTimer(); + this.closeHistoryPanel(); + this.dragging = false; + }.bind(this), false); +}; + + + +/** + * The main function called when a button is pressed + * + * @param value string The value of the button pressed + */ +Calculator.prototype.buttonEvent = function(value) { + switch (value) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + this.appendDigitToEquation(value); + break; + case '+': + case '*': + case '-': + case '/': + this.appendOperatorToEquation(value); + break; + case '.': + this.appendDecimalToEquation(); + break; + case '(': + case ')': + this.appendBracketToEquation(value); + break; + case '=': + this.equate(); + break; + case 'b': + this.backspace(); + break; + case 'c': + this.clearAll(); + break; + case '+-': + this.invertNumber(); + break; + case 'h': + this.openHistoryPanel(); + break; + } +}; + + + /** * Append digit to equation * * @param digit int The digit to append */ Calculator.prototype.appendDigitToEquation = function(digit) { - var lastInput = this.appstate.last, - currentNumber = this.getLastNum(); + var lastInput = this.appstate.last; switch (lastInput) { case null: - this.appendToEquation(digit, true); + this.appstate.input = [digit]; + this.appstate.last = digit; break; case '0': case '1': @@ -105,20 +287,25 @@ Calculator.prototype.appendDigitToEquation = function(digit) { case '8': case '9': case '.': + if (this.appstate.input.last() === '0') { + this.appstate.input.replaceLast(digit); + } + else { + this.appstate.input.appendToLast(digit); + } + this.appstate.last = digit; + break; case '(': case '*': case '/': case '+': case '-': - if (lastInput === '0' && this.appstate.input.length === 1) { - this.backspace(); - this.appendToEquation(digit); - } - else if (this.isValidNum(currentNumber + digit)) { - this.appendToEquation(digit); - } + this.appstate.input.push(digit); + this.appstate.last = digit; break; } + + this.updateDisplay(); }; @@ -127,10 +314,13 @@ Calculator.prototype.appendDigitToEquation = function(digit) { * Append decimal to equation */ Calculator.prototype.appendDecimalToEquation = function() { - var lastInput = this.appstate.last, - currentNumber = this.getLastNum(); + var lastInput = this.appstate.last; switch (lastInput) { + case null: + this.appstate.input = ['0.']; + this.appstate.last = '.'; + break; case '0': case '1': case '2': @@ -141,21 +331,22 @@ Calculator.prototype.appendDecimalToEquation = function() { case '7': case '8': case '9': - if (this.isValidNum(currentNumber + '.')) { - this.appendToEquation('.'); + if (this.isValidNum(this.appstate.input.last() + '.')) { + this.appstate.input.appendToLast('.'); + this.appstate.last = '.'; } break; - case null: - this.appendToEquation('0.', true); - break; case '(': case '*': case '/': case '+': case '-': - this.appendToEquation('0.'); + this.appstate.input.push('0.'); + this.appstate.last = '.'; break; } + + this.updateDisplay(); }; @@ -170,8 +361,6 @@ Calculator.prototype.appendOperatorToEquation = function(operator) { switch (lastInput) { case null: - this.appendToEquation(operator); - break; case '0': case '1': case '2': @@ -183,16 +372,19 @@ Calculator.prototype.appendOperatorToEquation = function(operator) { case '8': case '9': case ')': - this.appendToEquation(operator); + this.appstate.input.push(operator); + this.appstate.last = operator; break; case '*': case '/': case '+': case '-': - this.backspace(); - this.appendToEquation(operator); + this.appstate.input.replaceLast(operator); + this.appstate.last = operator; break; } + + this.updateDisplay(); }; @@ -208,15 +400,30 @@ Calculator.prototype.appendBracketToEquation = function(bracket) { if (bracket === '(') { switch (lastInput) { case null: - this.appendToEquation('(', true); + this.appstate.input = ['(']; this.appstate.brackets += 1; + this.appstate.last = '('; break; case '*': case '/': case '+': case '-': case '(': - this.appendToEquation('('); + this.appstate.input.push('('); + this.appstate.brackets += 1; + this.appstate.last = '('; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + this.appstate.input.splice(this.appstate.input.length - 1, 0, '('); this.appstate.brackets += 1; break; } @@ -238,37 +445,13 @@ Calculator.prototype.appendBracketToEquation = function(bracket) { case '8': case '9': if (this.appstate.brackets > 0) { - this.appendToEquation(')'); + this.appstate.input.push(')'); this.appstate.brackets -= 1; + this.appstate.last = ')'; } break; } } -}; - - - -/** - * Append an operator, operand or bracket to the equation string - * Whenever the equation is updated, the display should also be updated. - * - * @param value string The value to add to the equation - * @param clear bool Should the appstate input be cleared first - */ -Calculator.prototype.appendToEquation = function(value, clear) { - if (clear) { - this.appstate.input = value; - } - else { - this.appstate.input += value; - } - - if (value === '0.') { - this.appstate.last = '.'; - } - else { - this.appstate.last = value; - } this.updateDisplay(); }; @@ -279,27 +462,20 @@ Calculator.prototype.appendToEquation = function(value, clear) { * Invert last number (from positive to negative and vise versa) */ Calculator.prototype.invertNumber = function() { - var str = this.appstate.input, - lastNum = this.getLastNum(), - len, - before; - - if (lastNum) { - len = lastNum.length; - before = str.charAt(str.length - len - 1); - - if (/[+*\-\/()]/.test(before) || before === '') { - - if (lastNum[0] === '-') { - lastNum = lastNum.substr(1, len); - } - else { - lastNum = '-' + lastNum; - } + var num; + + if (/[\d\.]/.test(this.appstate.last)) { + num = this.appstate.input.last(); + if (num.substr(0, 1) === '-') { + this.appstate.input.replaceLast(num.substr(1, num.length)); + } + else if (this.appstate.input[this.appstate.input.length - 2] === '-') { + this.appstate.input[this.appstate.input.length - 2] = '+'; + } + else if (this.isValidNum('-' + num)) { + this.appstate.input.replaceLast('-' + num); } - - this.appstate.input = str.substr(0, str.length - len) + lastNum; this.updateDisplay(); } @@ -312,13 +488,13 @@ Calculator.prototype.invertNumber = function() { * Evaluates the current equation string. */ Calculator.prototype.equate = function() { - var result = this.compute(this.appstate.input), - historyItem = {}; + var result = this.compute(); if (result !== null) { - historyItem.result = result; - historyItem.equ = this.appstate.input; - this.addHistoryItem(historyItem); + this.addHistoryItem({ + 'result': result, + 'equ': this.appstate.input + }); this.clearAll(result.toString()); } }; @@ -329,25 +505,56 @@ Calculator.prototype.equate = function() { * Remove the last input character */ Calculator.prototype.backspace = function() { - var input = this.appstate.input, - last = this.appstate.last; + var string; - if (last === '(') { - this.appstate.brackets -= 1; + if (this.appstate.input.length <= 1 && this.appstate.input.last().length <= 1) { + this.clearAll(); } - else if (last === ')') { - this.appstate.brackets += 1; + else if (this.appstate.last === null) { + this.clearAll(); } - - if (input.length > 1 && last !== null) { - this.appstate.input = input.slice(0, input.length - 1); - this.appstate.last = input.charAt(input.length - 2); + else { + switch(this.appstate.last) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '.': + if (this.appstate.input.last().length <= 1) { + this.appstate.input.pop(); + } + else { + string = this.appstate.input.last(); + this.appstate.input.replaceLast(string.slice(0, string.length - 1)); + } + break; + case '(': + this.appstate.brackets -= 1; + this.appstate.input.pop(); + break; + case ')': + this.appstate.brackets += 1; + this.appstate.input.pop(); + break; + case '*': + case '/': + case '+': + case '-': + this.appstate.input.pop(); + break; + } + + string = this.appstate.input.join(''); + this.appstate.last = string.charAt(string.length - 1); this.updateDisplay(); } - else { - this.clearAll(); - } }; @@ -379,7 +586,7 @@ Calculator.prototype.activateButton = function(id) { /** * Deactivate button * - * @param btn string The DOM node ID to deactivate + * @param btn string The DOM node to deactivate */ Calculator.prototype.deactivateButton = function(btn) { btn.classList.remove('active'); @@ -394,10 +601,10 @@ Calculator.prototype.deactivateButton = function(btn) { */ Calculator.prototype.clearAll = function(result) { if (result) { - this.appstate.input = result; + this.appstate.input = [result]; } else { - this.appstate.input = 0; + this.appstate.input = ['0']; } this.appstate.brackets = 0; @@ -417,14 +624,14 @@ Calculator.prototype.clearAll = function(result) { Calculator.prototype.isValidNum = function(num) { /** * Regex eplainaition: - * ^ Match at start of string - * \-? Optional negative - * 0 Zero, or - * 0(?!\.) Zero if followed by decimal, or - * ([1-9]{1}\d*) Exactly one 1-9 and zero or more digits, or - * \.(?!\.)\d* A decimal only if not followed by another decimal plus zero or more digits - * (\.\d*){0,1} Only one grouping of a decimal and zero or more digits - * $ Match end of string + * ^ Match at start of string + * \-? Optional negative + * 0| Zero, or + * 0(?!\.)| Zero if followed by decimal, or + * ([1-9]{1}\d*)| Exactly one 1-9 and zero or more digits, or + * \.(?!\.)\d* A decimal only if not followed by another decimal plus zero or more digits + * (\.\d*){0,1} Only one grouping of a decimal and zero or more digits + * $ Match end of string */ if (/^\-?(0|0(?!\.)|([1-9]{1}\d*)|\.(?!\.)\d*)(\.\d*){0,1}$/.test(num)) { return true; @@ -435,34 +642,11 @@ Calculator.prototype.isValidNum = function(num) { -/** - * Parses the last full number from the input string (eg. -42.63) - * - * return A full number - */ -Calculator.prototype.getLastNum = function() { - var str = this.appstate.input, - arr; - - if (str.length > 0) { - arr = str.match(/-?\d*\.?\d*$/); - - if (arr !== null) { - return arr[0]; - } - } - - return false; -}; - - - /** * Update the calculator display */ Calculator.prototype.updateDisplay = function() { - var eq = this.appstate.input.toString(), - result = this.compute(eq), + var result = this.compute(), activeBtn = document.querySelector('.active'); // Update the result @@ -471,7 +655,7 @@ Calculator.prototype.updateDisplay = function() { this.result.innerHTML = '' + result.toExponential(this.settings.decimals) + ''; } else { - this.result.innerHTML = '' + this.addCommas(result) + ''; + this.result.innerHTML = '' + this.addCommas(result) + ''; } this.resizeFont(); } @@ -496,7 +680,7 @@ Calculator.prototype.updateDisplay = function() { break; } - this.updateDisplayEquation(eq); + this.updateDisplayEquation(); this.saveAppState(); }; @@ -509,18 +693,18 @@ Calculator.prototype.updateDisplay = function() { * * @param equation string The equation string */ -Calculator.prototype.updateDisplayEquation = function(equation) { +Calculator.prototype.updateDisplayEquation = function() { var ele = document.getElementById('eq'), - i = equation.length, + equ = this.appstate.input.slice(), width; - ele.innerHTML = this.replaceOperators(equation); + ele.innerHTML = this.replaceOperators(equ); width = ele.offsetWidth; while (width > this.equation.offsetWidth - 24) { - ele.innerHTML = '...' + this.replaceOperators(equation.substr(equation.length - i, i)); + equ.splice(0, 1); + ele.innerHTML = '...' + this.replaceOperators(equ); width = ele.offsetWidth; - i -= 1; } }; @@ -529,19 +713,39 @@ Calculator.prototype.updateDisplayEquation = function(equation) { /** * Replace operators with display strings * - * @param str string The equation string to replace the operators in - * return string The new display string + * @param equ array The equation array to replace the operators in + * return string The new display HTML string */ -Calculator.prototype.replaceOperators = function(str) { - str = str.replace(/\//g, '÷'); - str = str.replace(/\*/g, '×'); - str = str.replace(/\+/g, '+'); - str = str.replace(/\-/g, ''); - str = str.replace(/\(/g, '('); - str = str.replace(/\)/g, ')'); +Calculator.prototype.replaceOperators = function(equ) { + var output = '', i; - return str; -}; + for (i = 0; i < equ.length; i += 1) { + switch(equ[i]) { + case '*': + output += '×'; + break; + case '/': + output += '÷'; + break; + case '+': + output += '+'; + break; + case '-': + output += ''; + break; + case '(': + output += '('; + break; + case ')': + output += ')'; + break; + default: + output += equ[i]; + } + } + + return output; +} @@ -550,21 +754,13 @@ Calculator.prototype.replaceOperators = function(str) { * Directly manipulates the DOM. */ Calculator.prototype.resizeFont = function() { - var size, displayWidth, textWidth; + var size = this.settings.fontsize; - size = this.settings.fontsize; this.result.style.fontSize = size + 'px'; - displayWidth = window.innerWidth - 24; - textWidth = this.result.childNodes[0].offsetWidth; - while (textWidth > displayWidth) { + while (this.result.childNodes[0].offsetWidth > window.innerWidth - 24) { size -= 1; this.result.style.fontSize = size + 'px'; - textWidth = this.result.childNodes[0].offsetWidth; - - if (size === 10) { - break; - } } }; @@ -577,23 +773,22 @@ Calculator.prototype.resizeFont = function() { * return string The new number string */ Calculator.prototype.addCommas = function(number) { - var parts, x, y, regx; + var parts = number.toString().split('.'), + regx = /(\d+)(\d{3})/, + integer = parts[0], + fraction = ''; - parts = number.toString().split('.'); - x = parts[0]; - if (parts.length > 1) { - y = '.' + parts[1]; - } - else { - y = ''; + // Add commas to integer part + while (regx.test(integer)) { + integer = integer.replace(regx, '$1' + ',' + '$2'); } - regx = /(\d+)(\d{3})/; - while (regx.test(x)) { - x = x.replace(regx, '$1' + ',' + '$2'); + // Add fractional part + if (parts.length > 1) { + fraction = '.' + parts[1]; } - return x + y; + return integer + fraction; }; @@ -604,8 +799,9 @@ Calculator.prototype.addCommas = function(number) { * @param equation string The equation string to compute * return double The result of the computation, else null if it cannot be computed */ -Calculator.prototype.compute = function(equation) { - var result, +Calculator.prototype.compute = function() { + var equation = this.appstate.input.join(''), + result, round = Math.pow(10, this.settings.decimals); try { @@ -644,17 +840,22 @@ Calculator.prototype.closeHistoryPanel = function() { * @param value string The history item string to add to the equation */ Calculator.prototype.appendHistoryItemToEquation = function(value) { - if (this.appstate.last === null) { - this.appstate.input = value; - } - else if (/[(+*\-\/]/.test(this.appstate.last)) { - this.appstate.input += value; + switch(this.appstate.last) { + case null: + this.appstate.input = [value]; + this.appstate.last = 1; + break; + case '*': + case '/': + case '+': + case '-': + case '(': + this.appstate.input.push(value); + this.appstate.last = 1; + break; } - this.appstate.last = value.charAt(value.length - 1); - this.updateDisplay(); - this.saveAppState(); }; @@ -665,18 +866,16 @@ Calculator.prototype.appendHistoryItemToEquation = function(value) { * @param item object The history item object to add */ Calculator.prototype.addHistoryItem = function(item) { - var i = this.history.length - 1, - ele; - - if (typeof this.history[i] !== 'object' || item.result !== this.history[i].result) { - while (this.history.length >= this.settings.history) { - this.history.shift(); - ele = this.historyList.childNodes[i]; - ele.parentNode.removeChild(ele); - i -= 1; - } + var last = this.history.first() || {result: null, equ: []}, + currentLen = this.history.length, + newLen = 0; + + if (this.replaceOperators(item.equ) !== this.replaceOperators(last.equ)) { + newLen = this.history.unshift(item); - this.history.push(item); + if (newLen > this.settings.history) { + this.history.splice(this.settings.history, newLen - currentLen); + } this.flashButton('btn-history'); this.appendToHistoryList(item); @@ -686,6 +885,51 @@ Calculator.prototype.addHistoryItem = function(item) { +/** + * Create history button item element + * Includes
  • ,