1 /**
  2  * @class BaseCalc
  3  * @constructor
  4  * @description Base Calculator class handles common configuration options, provides utility methods, an interface for extending, 
  5  * and runs calculator's initialize method on page load if it exists.
  6  * @param {Object} config Configuration object with the following options:
  7  * <ul>
  8  * 	<li>total.id : (Optional) Element ID to place end result of column calculation</li>
  9  *  <li>total.operator : (Optional) ( +, -, *, x, / ) Mathematical operator to apply against each row to produce end result.  Defaults to '+'</li>
 10  *  <li>calcOnLoad : (Optional) Defaults to false. If true, on page load the calculate method is fired</li>
 11  *  <li>registerListeners : (Optional) Defaults to false. If true, event listeners are attached to inputs that fire the calculate method</li>
 12  * </ul>
 13  */
 14 slikcalc.BaseCalc = function(config) {
 15     config.total = config.total || {};
 16 	this.totalId = config.total.id || null;
 17 	this.totalOperator = config.total.operator || '+';
 18 	this.calcOnLoad = config.calcOnLoad || false;
 19 	this.calculationComplete = slikcalc.createCustomEvent('calculationComplete');
 20 	this.registerListeners = config.registerListeners || false;
 21 	slikcalc.addOnLoad(this.baseInitialize, this);
 22 };
 23 
 24 slikcalc.BaseCalc.prototype = {
 25 
 26 	/**
 27 	 * @description Reference to slikcalc custom event to fire when the calculation has been 
 28 	 * completed for this calculator
 29 	 */
 30 	calculationComplete : null,
 31 	
 32 	/**
 33 	 * @description Internal value of the time when the last keyup event fired used to make sure and
 34 	 * fire the calculate event only after there is a delay in typing for performance reasons
 35 	 */
 36 	lastKeyUp : null,
 37 	
 38 	/**
 39 	 * @description Internal value tracking the number of calculations triggered used to prevent callbacks from firing out of synch
 40 	 */
 41 	calculations : 0,
 42 	
 43 	/**
 44 	 * @description Element ID for the total value of the calculator
 45 	 */
 46 	totalId : null,
 47 	
 48 	/**
 49 	 * @description Mathematical operator to apply against each row to produce end result ( +, -, *, x, / )
 50 	 */
 51 	totalOperator : null,
 52 	
 53 	/**
 54 	 * @description Boolean value to signal if the calculator should fire initially when the page loads
 55 	 */
 56 	calcOnLoad : false,
 57 	
 58 	/**
 59 	 * @description Boolean value indicating if event listeners should be placed on relevant elements to tracking key events and clicking
 60 	 */
 61 	registerListeners : false,
 62 	
 63 	/**
 64 	 * @description Configuration value for the pause in keyup events to wait for before calculating.  
 65 	 * Setting this to zero will cause calculations to perform on each keyup event, which could become costly with many calculators
 66 	 * chained together
 67 	 */
 68 	keyupDelay: 600,
 69 	
 70 	/**
 71 	 * @description Internal boolean to track if the calculator has been initialized yet
 72 	 */
 73 	initialized : false,
 74 	
 75 	/**
 76 	 * @description Base initialize method to handle calling subclass initialize(), and sets initialized property.
 77 	 * Also calls processCalculation() if calcOnLoad is true
 78 	 */
 79 	baseInitialize : function() {
 80 		if(this.initialized === false) {
 81 			this.initialized = true;
 82 			if(this.initialize !== undefined && typeof this.initialize === 'function') {
 83 			    this.initialize();
 84 			}
 85 			if(this.calcOnLoad === true) {
 86 				this.processCalculation();
 87 			}
 88 		}
 89 	},
 90 	
 91 	/**
 92 	 * @description Sets up event chaining for BaseCalc objects.  The object passed in is returned to allow for a fluent interface
 93 	 * this.calculate will be called after dependCalc.calculate
 94 	 * @param {BaseCalc} dependCalc BaseCalc object to attach event to
 95 	 */
 96 	dependsOn : function(dependCalc) {
 97 		slikcalc.bindEvent(dependCalc.calculationComplete, this.processCalculation, this);
 98 		return dependCalc;
 99 	},
100 	
101 	/**
102 	 * @description Sets up event chaining for BaseCalc objects.  The object passed in is returned to allow for a fluent interface
103 	 * this.calculate will be called before triggeredCalc.calculate
104 	 * @param {BaseCalc} triggeredCalc BaseCalc object to attach event to
105 	 */
106 	triggers : function(triggeredCalc) {
107 		slikcalc.bindEvent(this.calculationComplete, triggeredCalc.processCalculation, triggeredCalc);
108 		return triggeredCalc;
109 	},
110 	
111 	/**
112 	 * @description Wrapper method to trigger processCalculation when there is a pause in users key events
113 	 */
114 	keyupEvent : function() {
115 		this.lastKeyup = new Date().getTime();
116 		this.calculations = this.calculations + 1;
117 		var that = this, calculation = this.calculations;
118 		setTimeout(function() {
119 			var currentTime = new Date().getTime(), difference = currentTime - that.lastKeyup;
120 			if(calculation == that.calculations && difference > that.keyupDelay) {
121 				that.processCalculation();
122 			}
123 		}, (this.keyupDelay+100));
124 	},
125 	
126 	/**
127 	 * @description Calculates the total amount, dependant upon the totalOperator value.  
128 	 * Seperated into conditional statements for better performance than using 'eval()', and to handle operator unique functionality
129 	 * @param {Float} total Initial value of total.
130 	 * @param {Float} amount New amount to calculate in conjunction with total.
131 	 */
132 	calculateTotal : function(total, amount) {
133 		if(this.totalOperator === '+') {
134 			total = total === null ? 0.00 : total;
135             total = total + amount;
136         }else if(this.totalOperator === '-') {
137 			if(total === null) {
138 				total = amount;
139 			}else {
140 				total = total - amount;
141 			}
142 		}else if(this.totalOperator === '*' || this.totalOperator === 'x') {
143 			total = total === null ? 1 : total;
144 			total = total * amount;
145 		}else if(this.totalOperator === '/') {
146 			if(total === null) {
147 				total = amount;
148 			}else {
149 				total = total / amount;
150 			}
151 		}
152 		return total;
153 	},
154 	
155 	/**
156 	 * @description Wrapper method for concrete class' `calculate` method
157 	 */
158 	processCalculation: function() {
159 		if(this.initialized === false) {
160 			this.baseInitialize();
161 		}
162         this.calculate();
163     	slikcalc.fireEvent(this.calculationComplete);
164 	},
165 	
166 	/**
167 	 * @description Abstract method to be implemented in sub-classes.
168 	 */
169 	calculate : function() {
170 		throw new Error('Must implement calculate method in sub-class of BaseCalc');
171 	}
172 };