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 }());