RT#37632: Credit card validation [saving from payment.cgi]
[freeside.git] / httemplate / elements / masked_input_1.3.js
1 /**
2  * AW Masked Input
3  * @version 1.3
4  * @author Kendall Conrad
5  * @url http://www.angelwatt.com/coding/masked_input.php
6  * @created 2008-12-16
7  * @modified 2013-08-19
8  * @license This work is licensed under a Creative Commons
9  *  Attribution-Share Alike 3.0 United States License
10  *  http://creativecommons.org/licenses/by-sa/3.0/us/
11  *
12  * @param scope The object to attach MaskedInput to.
13  */
14 (function(scope) {
15         'use strict';
16
17         /**
18          * MaskedInput takes many possible arguments described below.
19          * Note: req = required, opt = optional
20          * @param {object} args {
21          *  -elm [req] text input node to apply the mask on
22          *  -format [req] string format for the mask
23          *  -allowed [opt, '0123456789'] string with chars allowed to be typed
24          *  -sep [opt, '\/:-'] string of char(s) used as separators in mask
25          *  -typeon [opt, '_YMDhms'] string of chars in mask that can be typed on
26          *  -onfilled [opt, null] function to run when the format is filled in
27          *  -onbadkey [opt, null] function to run when user types a unallowed key
28          *  -badkeywait [opt, 0] used with onbadkey. Indicates how long (in ms)
29          *   to lock text input for onbadkey function to run
30          *  -preserve [opt, true] whether to preserve existing text in
31          *   field during init.
32          * }
33          * @returns MaskedInput
34          */
35         scope.MaskedInput = function(args) {
36                 // Ensure passing in valid argument
37                 if (!args || !args.elm || !args.format) {
38                         return null;
39                 }
40                 // Ensure use of 'new'
41                 if (!(this instanceof scope.MaskedInput)) {
42                         return new scope.MaskedInput(args);
43                 }
44                 // Initialize variables
45                 var self = this,
46                         el = args.elm,
47                         format = args.format,
48                         allowed = args.allowed || '0123456789',
49                         sep = args.separator || '\/:-',
50                         open = args.typeon || '_YMDhms',
51                         onbadkey = args.onbadkey || function() {},
52                         onfilled = args.onfilled || function() {},
53                         badwait = args.badkeywait || 0,
54                         preserve = args.hasOwnProperty('preserve') ? !!args.preserve : true,
55                         // ----
56                         enabled = true,
57                         locked = false,
58                         startText = format,
59                 /**
60                  * Add events to objects.
61                  */
62                 evtAdd = (function() {
63                         if (window.addEventListener) {
64                                 return function(obj, type, fx, capture) {
65                                         obj.addEventListener(type, fx,
66                                                         (capture === undefined) ? false : capture);
67                                 };
68                         }
69                         if (window.attachEvent) {
70                                 return function(obj, type, fx) {
71                                         obj.attachEvent('on' + type, fx);
72                                 };
73                         }
74                         return function(obj, type, fx) {
75                                 obj['on' + type] = fx;
76                         };
77                 }()),
78                 /**
79                  * Checks whether the format has been completely filled out.
80                  * @return boolean if all typeon chars have been filled.
81                  */
82                 isFilled = function() {
83                         // Check if any typeon characters are left
84                         // Work from end of string as it's usually last filled
85                         for (var a = el.value.length - 1; a >= 0; a--) {
86                                 // Check against each typeon character
87                                 for (var c = 0, d = open.length; c < d; c++) {
88                                         // If one matches we don't need to check anymore
89                                         if (el.value[a] === open[c]) {
90                                                 return false;
91                                         }
92                                 }
93                         }
94                         return true;
95                 },
96                 /**
97                  * Gets the current position of the text cursor in a text field.
98                  * @param node a input or textarea HTML node.
99                  * @return int text cursor position index, or -1 if there was a problem.
100                  */
101                 getTextCursor = function(node) {
102                         try {
103                                 node.focus();
104                                 if (node.selectionStart >= 0) {
105                                         return node.selectionStart;
106                                 }
107                                 if (document.selection) {// IE
108                                         var rng = document.selection.createRange();
109                                         return -rng.moveStart('character', -node.value.length);
110                                 }
111                                 return -1;
112                         }
113                         catch (e) {
114                                 return -1;
115                         }
116                 },
117                 /**
118                  * Sets the text cursor in a text field to a specific position.
119                  * @param node a input or textarea HTML node.
120                  * @param pos int of the position to be placed.
121                  * @return boolean true is successful, false otherwise.
122                  */
123                 setTextCursor = function(node, pos) {
124                         try {
125                                 if (node.selectionStart) {
126                                         node.focus();
127                                         node.setSelectionRange(pos, pos);
128                                 }
129                                 else if (node.createTextRange) { // IE
130                                         var rng = node.createTextRange();
131                                         rng.move('character', pos);
132                                         rng.select();
133                                 }
134                         }
135                         catch (e) {
136                                 return false;
137                         }
138                         return true;
139                 },
140                 /**
141                  * Gets the keyboard input in usable way.
142                  * @param code integer character code
143                  * @return string representing character code
144                  */
145                 getKey = function(code) {
146                         code = code || window.event;
147                         var ch = '',
148                                 keyCode = code.which,
149                                 evt = code.type;
150                         if (keyCode === undefined || keyCode === null) {
151                                 keyCode = code.keyCode;
152                         }
153                         // no key, no play
154                         if (keyCode === undefined || keyCode === null) {
155                                 return '';
156                         }
157                         // deal with special keys
158                         switch (keyCode) {
159                                 case 8:
160                                         ch = 'bksp';
161                                         break;
162                                 case 46: // handle del and . both being 46
163                                         ch = (evt === 'keydown') ? 'del' : '.';
164                                         break;
165                                 case 16:
166                                         ch = 'shift';
167                                         break;
168                                 case 0: /*CRAP*/
169                                 case 9: /*TAB*/
170                                 case 13:/*ENTER*/
171                                         ch = 'etc';
172                                         break;
173                                 case 37:
174                                 case 38:
175                                 case 39:
176                                 case 40: // arrow keys
177                                         ch = (!code.shiftKey &&
178                                                         (code.charCode !== 39 && code.charCode !== undefined)) ?
179                                                         'etc' : String.fromCharCode(keyCode);
180                                         break;
181                                         // default to thinking it's a character or digit
182                                 default:
183                                         ch = String.fromCharCode(keyCode);
184                                         break;
185                         }
186                         return ch;
187                 },
188                 /**
189                  * Stop the event propogation chain.
190                  * @param evt Event to stop
191                  * @param ret boolean, used for IE to prevent default event
192                  */
193                 stopEvent = function(evt, ret) {
194                         // Stop default behavior the standard way
195                         if (evt.preventDefault) {
196                                 evt.preventDefault();
197                         }
198                         // Then there's IE
199                         evt.returnValue = ret || false;
200                 },
201                 /**
202                  * Updates the text field with the given key.
203                  * @param key string keyboard input.
204                  */
205                 update = function(key) {
206                         var p = getTextCursor(el),
207                                 c = el.value,
208                                 val = '',
209                                 cond = true;
210                         // Handle keys now
211                         switch (cond) {
212                                 // Allowed characters
213                                 case (allowed.indexOf(key) !== -1):
214                                         p = p + 1;
215                                         // if text cursor at end
216                                         if (p > format.length) {
217                                                 return false;
218                                         }
219                                         // Handle cases where user places cursor before separator
220                                         while (sep.indexOf(c.charAt(p - 1)) !== -1 && p <= format.length) {
221                                                 p = p + 1;
222                                         }
223                                         val = c.substr(0, p - 1) + key + c.substr(p);
224                                         // Move csor up a spot if next char is a separator char
225                                         if (allowed.indexOf(c.charAt(p)) === -1
226                                                         && open.indexOf(c.charAt(p)) === -1) {
227                                                 p = p + 1;
228                                         }
229                                         break;
230                                 case (key === 'bksp'): // backspace
231                                         p = p - 1;
232                                         // at start of field
233                                         if (p < 0) {
234                                                 return false;
235                                         }
236                                         // If previous char is a separator, move a little more
237                                         while (allowed.indexOf(c.charAt(p)) === -1
238                                                         && open.indexOf(c.charAt(p)) === -1
239                                                         && p > 1) {
240                                                 p = p - 1;
241                                         }
242                                         val = c.substr(0, p) + format.substr(p, 1) + c.substr(p + 1);
243                                         break;
244                                 case (key === 'del'): // forward delete
245                                         // at end of field
246                                         if (p >= c.length) {
247                                                 return false;
248                                         }
249                                         // If next char is a separator and not the end of the text field
250                                         while (sep.indexOf(c.charAt(p)) !== -1
251                                                         && c.charAt(p) !== '') {
252                                                 p = p + 1;
253                                         }
254                                         val = c.substr(0, p) + format.substr(p, 1) + c.substr(p + 1);
255                                         p = p + 1; // Move position forward
256                                         break;
257                                 case (key === 'etc'):
258                                         // Catch other allowed chars
259                                         return true;
260                                 default:
261                                         return false; // Ignore the rest
262                         }
263                         el.value = ''; // blank it first (Firefox issue)
264                         el.value = val; // put updated value back in
265                         setTextCursor(el, p); // Set the text cursor
266                         return false;
267                 },
268                 /**
269                  * Returns whether or not a given input is valid for the mask.
270                  * @param k string of character to check.
271                  * @return bool true if it's a valid character.
272                  */
273                 goodOnes = function(k) {
274                         // if not in allowed list, or invisible key action
275                         if (allowed.indexOf(k) === -1 && k !== 'bksp' && k !== 'del' && k !== 'etc') {
276                                 // Need to ensure cursor position not lost
277                                 var p = getTextCursor(el);
278                                 locked = true;
279                                 onbadkey(k);
280                                 // Hold lock long enough for onbadkey function to run
281                                 setTimeout(function() {
282                                         locked = false;
283                                         setTextCursor(el, p);
284                                 }, badwait);
285                                 return false;
286                         }
287                         return true;
288                 },
289                 /**
290                  * Handles the key down events.
291                  * @param e Event
292                  */
293                 keyHandlerDown = function(e) {
294                         if (!enabled) {
295                                 return true;
296                         }
297                         if (locked) {
298                                 stopEvent(e);
299                                 return false;
300                         }
301                         e = e || event;
302                         var key = getKey(e);
303                         // Stop copy and paste
304                         if ((e.metaKey || e.ctrlKey) && (key === 'X' || key === 'V')) {
305                                 stopEvent(e);
306                                 return false;
307                         }
308                         // Allow for OS commands
309                         if (e.metaKey || e.ctrlKey) {
310                                 return true;
311                         }
312                         if (el.value === '') {
313                                 el.value = format;
314                                 setTextCursor(el, 0);
315                         }
316                         // Only do update for bksp del
317                         if (key === 'bksp' || key === 'del') {
318                                 update(key);
319                                 stopEvent(e);
320                                 return false;
321                         }
322                         return true;
323                 },
324                 /**
325                  * Handles the key press events.
326                  * @param e Event
327                  */
328                 keyHandlerPress = function(e) {
329                         if (!enabled) {
330                                 return true;
331                         }
332                         if (locked) {
333                                 stopEvent(e);
334                                 return false;
335                         }
336                         e = e || event;
337                         var key = getKey(e);
338                         // Check if modifier key is being pressed; command
339                         if (key === 'etc' || e.metaKey || e.ctrlKey || e.altKey) {
340                                 return true;
341                         }
342                         if (key !== 'bksp' && key !== 'del' && key !== 'shift') {
343                                 if (!goodOnes(key)) {
344                                         stopEvent(e);
345                                         return false;
346                                 }
347                                 if (update(key)) {
348                                         if (isFilled()) {
349                                                 onfilled();
350                                         }
351                                         stopEvent(e, true);
352                                         return true;
353                                 }
354                                 if (isFilled()) {
355                                         onfilled();
356                                 }
357                                 stopEvent(e);
358                                 return false;
359                         }
360                         return false;
361                 },
362                 /**
363                  * Initialize the object.
364                  */
365                 init = function() {
366                         // Check if an input or textarea tag was passed in
367                         if (!el.tagName || (el.tagName.toUpperCase() !== 'INPUT'
368                                         && el.tagName.toUpperCase() !== 'TEXTAREA')) {
369                                 return null;
370                         }
371                         // Only place formatted text in field when not preserving
372                         // text or it's empty.
373                         if (!preserve || el.value === '') {
374                                 el.value = format;
375                         }
376                         // Assign events
377                         evtAdd(el, 'keydown', function(e) {
378                                 keyHandlerDown(e);
379                         });
380                         evtAdd(el, 'keypress', function(e) {
381                                 keyHandlerPress(e);
382                         });
383                         // Let us set the initial text state when focused
384                         evtAdd(el, 'focus', function() {
385                                 startText = el.value;
386                         });
387                         // Handle onChange event manually
388                         evtAdd(el, 'blur', function() {
389                                 if (el.value !== startText && el.onchange) {
390                                         el.onchange();
391                                 }
392                         });
393                         return self;
394                 };
395
396                 /**
397                  * Resets the text field so just the format is present.
398                  */
399                 self.resetField = function() {
400                         el.value = format;
401                 };
402
403                 /**
404                  * Set the allowed characters that can be used in the mask.
405                  * @param a string of characters that can be used.
406                  */
407                 self.setAllowed = function(a) {
408                         allowed = a;
409                         self.resetField();
410                 };
411
412                 /**
413                  * The format to be used in the mask.
414                  * @param f string of the format.
415                  */
416                 self.setFormat = function(f) {
417                         format = f;
418                         self.resetField();
419                 };
420
421                 /**
422                  * Set the characters to be used as separators.
423                  * @param s string representing the separator characters.
424                  */
425                 self.setSeparator = function(s) {
426                         sep = s;
427                         self.resetField();
428                 };
429
430                 /**
431                  * Set the characters that the user will be typing over.
432                  * @param t string representing the characters that will be typed over.
433                  */
434                 self.setTypeon = function(t) {
435                         open = t;
436                         self.resetField();
437                 };
438
439                 /**
440                  * Sets whether the mask is active.
441                  */
442                 self.setEnabled = function(enable) {
443                         enabled = enable;
444                 };
445
446                 /**
447                  * Local change for Freeside: sets the content of the field,
448                  * respecting formatting rules
449                  */
450                 self.setValue = function(value) {
451                         self.resetField();
452                         setTextCursor(el, 0);
453                         var i = 0; // index in value
454                         while (i < value.length && !isFilled()) {
455                           update(value[i]);
456                           i++;
457                         }
458                 }
459
460                 return init();
461         };
462 }(window));