1 /*! 2 * JavaScript Calculator v1.1 3 * http://wjbryant.com/projects/javascript-calculator/ 4 * 5 * Copyright (c) 2011 Bill Bryant 6 * Licensed under the MIT license 7 * http://opensource.org/licenses/mit-license.php 8 */ 9 10 /** 11 * @fileOverview JavaScript Calculator 12 * @author Bill Bryant 13 * @version 1.1 14 */ 15 16 /*jslint browser: true, maxerr: 50, indent: 4 */ 17 18 /** 19 * The JSCALC "namespace" / global object. 20 * 21 * @namespace 22 */ 23 var JSCALC; 24 if (typeof JSCALC !== 'object') { 25 JSCALC = {}; 26 } 27 28 (function () { 29 'use strict'; 30 31 var win = window, 32 document = win.document, 33 version = '1.1', 34 calculators = {}, // an object containing all the calculators created 35 origUnload = win.onunload, 36 styleAdded = false, 37 nextID = 0, 38 39 /** 40 * A simple implementation of getElementsByClassName. Checks for one 41 * class name and does not account for differences between NodeList and 42 * Array. 43 * 44 * @param {String} className the class name 45 * @return {NodeList|Array} a NodeList or Array of elements 46 * 47 * @function 48 * @ignore 49 */ 50 getElementsByClassName = document.getElementsByClassName ? 51 function (className) { 52 return document.getElementsByClassName(className); 53 } : 54 document.querySelectorAll ? 55 function (className) { 56 return document.querySelectorAll('.' + className); 57 } : 58 document.evaluate ? 59 function (className) { 60 var elems = document.evaluate( 61 "//*[contains(concat(' ', @class, ' '), ' " + 62 className + " ')]", 63 document, 64 null, 65 0, 66 null 67 ), 68 elem = elems.iterateNext(), 69 results = []; 70 while (elem) { 71 results[results.length] = elem; 72 elem = elems.iterateNext(); 73 } 74 return results; 75 } : 76 function (className) { 77 var i, 78 elems = document.getElementsByTagName('*'), 79 num = elems.length, 80 pattern = new RegExp('(^|\\s)' + className + '(\\s|$)'), 81 results = []; 82 for (i = 0; i < num; i += 1) { 83 if (pattern.test(elems[i].className)) { 84 results[results.length] = elems[i]; 85 } 86 } 87 return results; 88 }; 89 90 /** 91 * Creates a new calculator in the specified container element (module). 92 * 93 * @param {Element} calcMod the element to contain the calculator 94 * @return {Object} a Calculator object 95 * 96 * @ignore 97 */ 98 function createCalc(calcMod) { 99 var forms, 100 form, 101 display, 102 total = 0, 103 operation, 104 clearNext = true, 105 dot = /\./, 106 lastNum = null, 107 getLastNum = false, 108 lastKeyDown, 109 operationPressed = false, // whether an operation was the last key pressed 110 calcObj = {}, 111 id = nextID; 112 113 /** 114 * Performs the basic mathematical operations (addition, subtraction, 115 * multiplication, division) on the current total with the given 116 * value for the current operation. 117 * 118 * @param {Number} val the value to use in the calculation with the total 119 * 120 * @ignore 121 */ 122 function calc(val) { 123 switch (operation) { 124 case '+': 125 total += val; 126 break; 127 case '-': 128 total -= val; 129 break; 130 case '*': 131 total *= val; 132 break; 133 case '/': 134 total /= val; 135 break; 136 } 137 display.value = total; 138 } 139 140 /** 141 * This function handles input for the form's keydown, keypress and 142 * click events. Any keypresses that are not related to a calculator 143 * function are not allowed. 144 * 145 * @return {Boolean} whether the default action is allowed 146 * 147 * @ignore 148 */ 149 function handleInput(e) { 150 e = e || win.event; 151 152 var key, // the key (char) that was pressed / clicked 153 code, // the key code 154 val, // the numeric value of the display 155 target, // the target element of the event 156 isOperation = false; // whether the button pressed is an operation 157 158 // this switch statement sets the key variable 159 switch (e.type) { 160 case 'keydown': 161 lastKeyDown = code = e.keyCode; 162 163 switch (code) { 164 case 27: 165 // escape 166 key = 'C'; 167 break; 168 case 8: 169 // backspace 170 key = 'Backspace'; 171 break; 172 case 46: 173 // delete 174 key = 'CE'; 175 break; 176 default: 177 // allow all other keys (enter, tab, numbers, letters, etc.) 178 return true; 179 } 180 break; 181 case 'keypress': 182 // most browsers provide the charCode property when the keypress event is fired 183 // IE and Opera provide the character code in the keyCode property instead 184 code = e.charCode || e.keyCode; 185 186 // this event is fired for all keys in Firefox and Opera 187 // because of this, we need to check the last keyCode 188 // from the keydown event for certain keys 189 190 // allow enter, tab and left and right arrow keys 191 // the enter key fires the keypress event in all browsers 192 // the other keys are handled here for Firefox and Opera 193 if (code === 13 || code === 9 || lastKeyDown === 37 || lastKeyDown === 39) { 194 return true; 195 } 196 // these keys are handled on keydown (escape, backspace, delete) 197 // this is for Firefox and Opera (and sometimes IE for the escape key) 198 if (code === 27 || code === 8 || lastKeyDown === 46) { 199 return false; 200 } 201 202 // get the key character in lower case 203 if (lastKeyDown === 188) { 204 key = '.'; // allow the comma key to be used for a decimal point 205 } else { 206 key = String.fromCharCode(code).toLowerCase(); 207 } 208 break; 209 case 'click': 210 target = e.target || e.srcElement; 211 if (target.tagName === 'INPUT' && target.type === 'button') { 212 key = target.value; 213 } else { 214 return true; 215 } 216 break; 217 case 'calculatorPressMethod': 218 // special case for the press method of the calculator object 219 key = e.calckey; 220 break; 221 default: 222 // the switch statement should never make it here 223 // this is just a safeguard 224 return true; 225 } 226 227 val = parseFloat(display.value); 228 229 switch (key) { 230 case '0': 231 case '1': 232 case '2': 233 case '3': 234 case '4': 235 case '5': 236 case '6': 237 case '7': 238 case '8': 239 case '9': 240 case '.': 241 // don't allow more than one decimal point 242 if (!(key === '.' && dot.test(display.value))) { 243 if (clearNext) { 244 display.value = ''; 245 clearNext = false; 246 } 247 display.value += key; 248 } 249 break; 250 case '*': 251 case '+': 252 case '-': 253 case '/': 254 // if an operation was the last key pressed, 255 // do nothing but change the current operation 256 if (!operationPressed) { 257 if (total === 0 || lastNum !== null) { 258 total = val; 259 } else { 260 calc(val); 261 } 262 lastNum = null; 263 getLastNum = true; 264 clearNext = true; 265 } 266 operation = key; 267 isOperation = true; 268 break; 269 case 'C': 270 display.blur(); // so Firefox can clear display when escape key is pressed 271 total = 0; 272 operation = ''; 273 clearNext = true; 274 lastNum = null; 275 getLastNum = false; 276 display.value = '0'; 277 break; 278 case 'CE': 279 display.value = '0'; 280 clearNext = true; 281 break; 282 case 'Backspace': 283 display.value = display.value.slice(0, display.value.length - 1); 284 break; 285 case '+/-': 286 display.value = val * -1; 287 break; 288 case '%': 289 if (val) { 290 display.value = total * val / 100; 291 } 292 break; 293 case 'sqrt': 294 if (val >= 0) { 295 display.value = Math.sqrt(val); 296 } else { 297 display.value = 'Invalid input for function'; 298 } 299 break; 300 case 'a': 301 case 'c': 302 case 'v': 303 case 'x': 304 // allow select all, copy, paste and cut key combinations 305 if (e.ctrlKey) { 306 return true; 307 } 308 break; 309 case '1/x': 310 case 'r': 311 if (val) { 312 display.value = 1 / val; 313 } else { 314 display.value = 'Cannot divide by zero'; 315 } 316 break; 317 case '=': 318 form.onsubmit(); 319 break; 320 } 321 322 operationPressed = isOperation; 323 display.focus(); 324 return false; 325 } 326 327 // increment the ID counter 328 nextID += 1; 329 330 // create the calculator elements 331 calcMod.innerHTML += '<div class="calcContainer"><form action=""><h1>Calculator</h1><div><input type="text" class="calcDisplay" /><input type="button" value="Backspace" class="calcClear calcFirst calcFunction" /><input type="button" value="CE" class="calcClear calcFunction" /><input type="button" value="C" class="calcClear calcFunction" /><input type="button" value="7" class="calcFirst calcInput" /><input type="button" value="8" class="calcInput" /><input type="button" value="9" class="calcInput" /><input type="button" value="/" class="calcFunction" /><input type="button" value="sqrt" class="calcInput" /><input type="button" value="4" class="calcFirst calcInput" /><input type="button" value="5" class="calcInput" /><input type="button" value="6" class="calcInput" /><input type="button" value="*" class="calcFunction" /><input type="button" value="%" class="calcInput" /><input type="button" value="1" class="calcFirst calcInput" /><input type="button" value="2" class="calcInput" /><input type="button" value="3" class="calcInput" /><input type="button" value="-" class="calcFunction" /><input type="button" value="1/x" class="calcInput" /><input type="button" value="0" class="calcFirst calcInput" /><input type="button" value="+/-" class="calcInput" /><input type="button" value="." class="calcInput" /><input type="button" value="+" class="calcFunction" /><input type="submit" value="=" class="calcFunction" /></div></form></div>'; 332 333 // get the calculator inputs 334 forms = calcMod.getElementsByTagName('form'); 335 form = forms[forms.length - 1]; // make sure it's the one that was just added 336 display = form.getElementsByTagName('input')[0]; 337 display.setAttribute('autocomplete', 'off'); 338 display.value = '0'; 339 display.onkeydown = display.onkeypress = form.onclick = handleInput; 340 341 /** 342 * Calculates the value of the last entered operation and displays the result. 343 * 344 * @return {Boolean} always returns false to prevent the form from being submitted 345 * 346 * @ignore 347 */ 348 form.onsubmit = function () { 349 if (getLastNum) { 350 lastNum = parseFloat(display.value) || 0; 351 getLastNum = false; 352 } 353 calc(lastNum); 354 clearNext = true; 355 display.focus(); 356 return false; 357 }; 358 359 /** 360 * The Calculator class. Calculator objects are returned by some 361 * methods of the JSCALC object. 362 * 363 * @name Calculator 364 * @class 365 * @private 366 */ 367 368 /** 369 * Gives focus to the calculator display. 370 * 371 * @function 372 * @name focus 373 * @memberOf Calculator.prototype 374 */ 375 calcObj.focus = function () { 376 display.focus(); 377 }; 378 379 /** 380 * Simulates pressing a button on the calculator. 381 * 382 * @param {Number|String} button the button(s) to press - a number can 383 * represent multiple buttons, but a 384 * string can only represent one 385 * @return {Object} the Calculator object 386 * 387 * @function 388 * @name press 389 * @memberOf Calculator.prototype 390 */ 391 calcObj.press = function (button) { 392 var buttons, 393 num, 394 i; 395 396 // if button is a number, convert it to an array of digits as strings 397 if (typeof button === 'number') { 398 buttons = button.toString().split(''); 399 } else if (typeof button === 'string' && button) { 400 buttons = [button]; 401 } else { 402 // invalid argument 403 return this; // do nothing, but still allow method chaining 404 } 405 406 num = buttons.length; 407 for (i = 0; i < num; i += 1) { 408 handleInput({ 409 type: 'calculatorPressMethod', 410 calckey: buttons[i] 411 }); 412 } 413 414 return this; // allow method chaining 415 }; 416 417 /** 418 * Removes the calculator and sets the Calculator object to null. 419 * 420 * @function 421 * @name remove 422 * @memberOf Calculator.prototype 423 */ 424 calcObj.remove = function () { 425 display.onkeydown = display.onkeypress = form.onclick = null; 426 calcMod.removeChild(form.parentNode); 427 delete calculators[id]; 428 calcObj = null; 429 }; 430 431 /** 432 * A reference to the element that contains the calculator. 433 * 434 * @name container 435 * @memberOf Calculator.prototype 436 */ 437 calcObj.container = calcMod; 438 439 calculators[id] = calcObj; // keep track of all calculators 440 441 return calcObj; 442 } 443 444 /** 445 * Gets the Calculator object associated with the calculator contained in 446 * the specified element. 447 * 448 * @param {Element} container the element containing the calculator 449 * @return {Object} the Calculator object or false if none exists 450 */ 451 JSCALC.get = function (container) { 452 // if the container argument is not an element node, do nothing 453 if (!container || container.nodeType !== 1) { 454 return; 455 } 456 457 var id, 458 calcs = calculators, 459 calc; 460 461 for (id in calcs) { 462 if (calcs.hasOwnProperty(id)) { 463 if (container === calcs[id].container) { 464 calc = calcs[id]; 465 break; 466 } 467 } 468 } 469 470 return calc || false; 471 }; 472 473 /** 474 * Gets the Calculator objects for all the calculators on the page. 475 * 476 * @return {Array} an array of Calculator objects 477 */ 478 JSCALC.getCalcs = function () { 479 var id, 480 calcArray = [], 481 calcs = calculators; 482 483 // the calculators array may be sparse, so copy all objects from it 484 // into a dense array and return that instead 485 for (id in calcs) { 486 if (calcs.hasOwnProperty(id)) { 487 calcArray[calcArray.length] = calcs[id]; 488 } 489 } 490 491 return calcArray; 492 }; 493 494 /** 495 * Specifies the CSS file location. By default, this property is set 496 * automatically the first time JSCALC.init() is called. Set this property 497 * before calling JSCALC.init() to override the automatic value. 498 * 499 * @name css 500 * @memberOf JSCALC 501 */ 502 503 /** 504 * Creates calculators. 505 * 506 * @param {String|Element} elem (optional) the element in which to create the calculator 507 * @return {Object|Array} If an argument is specified, the Calculator object or 508 * false is returned. If no (or invalid) arguments are 509 * specified, an array of Calculator objects or an empty 510 * array is returned. 511 */ 512 JSCALC.init = function (elem) { 513 var calcStyle, 514 calcMods = [], 515 args = false, 516 calcMod, 517 len, 518 i, 519 scripts, 520 numScripts, 521 src, 522 separatorPos, 523 newCalcs = []; 524 525 // treat a string argument as an element id 526 if (typeof elem === 'string') { 527 elem = document.getElementById(elem); 528 } 529 530 // if the argument is an element object or an element was found by id 531 if (typeof elem === 'object' && elem.nodeType === 1) { 532 // add the "calc" class name to the element 533 if (elem.className) { 534 if (elem.className.indexOf('calc') === -1) { 535 elem.className += ' calc'; 536 } 537 } else { 538 elem.className = 'calc'; 539 } 540 541 // add the element to the array of calculator modules to be initialized 542 calcMods[0] = elem; 543 args = true; 544 } else { 545 // if an element node was not found or specified, get all elements 546 // with a class name of "calc" 547 calcMods = getElementsByClassName('calc'); 548 } 549 550 len = calcMods.length; 551 552 // if there is at least one element in the array 553 if (len) { 554 // add the style sheet if it isn't already added 555 if (!styleAdded) { 556 // try to guess the CSS file location if it is not specified 557 if (!JSCALC.css) { 558 scripts = document.getElementsByTagName('script'); 559 numScripts = scripts.length; 560 561 // loop through the script elements and find this script by 562 // checking the src property 563 for (i = 0; i < numScripts; i += 1) { 564 src = scripts[i].src; 565 566 if (src.indexOf('calc-' + version) !== -1) { 567 // guess the CSS file path based on the js file path 568 separatorPos = src.lastIndexOf('/'); 569 JSCALC.css = (separatorPos !== -1 ? src.slice(0, separatorPos + 1) : '') + 570 'calc-' + version + '-min.css'; 571 break; 572 } 573 } 574 } 575 576 // make sure the stylesheet location is set before adding the stylesheet 577 if (JSCALC.css) { 578 calcStyle = document.createElement('link'); 579 calcStyle.type = 'text/css'; 580 calcStyle.rel = 'stylesheet'; 581 calcStyle.href = JSCALC.css; 582 (document.head || document.getElementsByTagName('head')[0]).appendChild(calcStyle); 583 styleAdded = true; 584 } 585 } 586 587 // loop through the array and create a calculator in each element 588 for (i = 0; i < len; i += 1) { 589 calcMod = calcMods[i]; 590 591 // check to ensure a calculator does not already exist in the 592 // specified element 593 if (!JSCALC.get(calcMod)) { 594 newCalcs[newCalcs.length] = createCalc(calcMod); 595 } 596 } 597 } 598 599 // if an argument was specified, return a single object or false if one 600 // could not be created 601 // else, return the array of objects even if it is empty 602 return args ? (newCalcs[0] || false) : newCalcs; 603 }; 604 605 /** 606 * Removes all calculators. 607 */ 608 JSCALC.removeAll = function () { 609 var id, 610 calcs = calculators; 611 612 // remove each calculator in the calculators array (calcs) 613 for (id in calcs) { 614 if (calcs.hasOwnProperty(id)) { 615 calcs[id].remove(); 616 } 617 } 618 }; 619 620 // remove all event handlers on window unload 621 // this will prevent a memory leak in older versions of IE 622 if (win.attachEvent) { 623 win.attachEvent('onunload', JSCALC.removeAll); 624 } else { 625 win.onunload = function (e) { 626 JSCALC.removeAll(); 627 // preserve original window unload event handler 628 if (origUnload) { 629 return origUnload(e); 630 } 631 }; 632 } 633 }());