summaryrefslogtreecommitdiff
path: root/httemplate/elements/masked_input_1.3.js
blob: 54e38ac86c79b62a6e9632b086c6bdc37c830b74 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
/**
 * AW Masked Input
 * @version 1.3
 * @author Kendall Conrad
 * @url http://www.angelwatt.com/coding/masked_input.php
 * @created 2008-12-16
 * @modified 2013-08-19
 * @license This work is licensed under a Creative Commons
 *  Attribution-Share Alike 3.0 United States License
 *  http://creativecommons.org/licenses/by-sa/3.0/us/
 *
 * @param scope The object to attach MaskedInput to.
 */
(function(scope) {
	'use strict';

	/**
	 * MaskedInput takes many possible arguments described below.
	 * Note: req = required, opt = optional
	 * @param {object} args {
	 *  -elm [req] text input node to apply the mask on
	 *  -format [req] string format for the mask
	 *  -allowed [opt, '0123456789'] string with chars allowed to be typed
	 *  -sep [opt, '\/:-'] string of char(s) used as separators in mask
	 *  -typeon [opt, '_YMDhms'] string of chars in mask that can be typed on
	 *  -onfilled [opt, null] function to run when the format is filled in
	 *  -onbadkey [opt, null] function to run when user types a unallowed key
	 *  -badkeywait [opt, 0] used with onbadkey. Indicates how long (in ms)
	 *   to lock text input for onbadkey function to run
	 *  -preserve [opt, true] whether to preserve existing text in
	 *   field during init.
	 * }
	 * @returns MaskedInput
	 */
	scope.MaskedInput = function(args) {
		// Ensure passing in valid argument
		if (!args || !args.elm || !args.format) {
			return null;
		}
		// Ensure use of 'new'
		if (!(this instanceof scope.MaskedInput)) {
			return new scope.MaskedInput(args);
		}
		// Initialize variables
		var self = this,
			el = args.elm,
			format = args.format,
			allowed = args.allowed || '0123456789',
			sep = args.separator || '\/:-',
			open = args.typeon || '_YMDhms',
			onbadkey = args.onbadkey || function() {},
			onfilled = args.onfilled || function() {},
			badwait = args.badkeywait || 0,
			preserve = args.hasOwnProperty('preserve') ? !!args.preserve : true,
			// ----
			enabled = true,
			locked = false,
			startText = format,
		/**
		 * Add events to objects.
		 */
		evtAdd = (function() {
			if (window.addEventListener) {
				return function(obj, type, fx, capture) {
					obj.addEventListener(type, fx,
							(capture === undefined) ? false : capture);
				};
			}
			if (window.attachEvent) {
				return function(obj, type, fx) {
					obj.attachEvent('on' + type, fx);
				};
			}
			return function(obj, type, fx) {
				obj['on' + type] = fx;
			};
		}()),
		/**
		 * Checks whether the format has been completely filled out.
		 * @return boolean if all typeon chars have been filled.
		 */
		isFilled = function() {
			// Check if any typeon characters are left
			// Work from end of string as it's usually last filled
			for (var a = el.value.length - 1; a >= 0; a--) {
				// Check against each typeon character
				for (var c = 0, d = open.length; c < d; c++) {
					// If one matches we don't need to check anymore
					if (el.value[a] === open[c]) {
						return false;
					}
				}
			}
			return true;
		},
		/**
		 * Gets the current position of the text cursor in a text field.
		 * @param node a input or textarea HTML node.
		 * @return int text cursor position index, or -1 if there was a problem.
		 */
		getTextCursor = function(node) {
			try {
				node.focus();
				if (node.selectionStart >= 0) {
					return node.selectionStart;
				}
				if (document.selection) {// IE
					var rng = document.selection.createRange();
					return -rng.moveStart('character', -node.value.length);
				}
				return -1;
			}
			catch (e) {
				return -1;
			}
		},
		/**
		 * Sets the text cursor in a text field to a specific position.
		 * @param node a input or textarea HTML node.
		 * @param pos int of the position to be placed.
		 * @return boolean true is successful, false otherwise.
		 */
		setTextCursor = function(node, pos) {
			try {
				if (node.selectionStart) {
					node.focus();
					node.setSelectionRange(pos, pos);
				}
				else if (node.createTextRange) { // IE
					var rng = node.createTextRange();
					rng.move('character', pos);
					rng.select();
				}
			}
			catch (e) {
				return false;
			}
			return true;
		},
		/**
		 * Gets the keyboard input in usable way.
		 * @param code integer character code
		 * @return string representing character code
		 */
		getKey = function(code) {
			code = code || window.event;
			var ch = '',
				keyCode = code.which,
				evt = code.type;
			if (keyCode === undefined || keyCode === null) {
				keyCode = code.keyCode;
			}
			// no key, no play
			if (keyCode === undefined || keyCode === null) {
				return '';
			}
			// deal with special keys
			switch (keyCode) {
				case 8:
					ch = 'bksp';
					break;
				case 46: // handle del and . both being 46
					ch = (evt === 'keydown') ? 'del' : '.';
					break;
				case 16:
					ch = 'shift';
					break;
				case 0: /*CRAP*/
				case 9: /*TAB*/
				case 13:/*ENTER*/
					ch = 'etc';
					break;
				case 37:
				case 38:
				case 39:
				case 40: // arrow keys
					ch = (!code.shiftKey &&
							(code.charCode !== 39 && code.charCode !== undefined)) ?
							'etc' : String.fromCharCode(keyCode);
					break;
					// default to thinking it's a character or digit
				default:
					ch = String.fromCharCode(keyCode);
					break;
			}
			return ch;
		},
		/**
		 * Stop the event propogation chain.
		 * @param evt Event to stop
		 * @param ret boolean, used for IE to prevent default event
		 */
		stopEvent = function(evt, ret) {
			// Stop default behavior the standard way
			if (evt.preventDefault) {
				evt.preventDefault();
			}
			// Then there's IE
			evt.returnValue = ret || false;
		},
		/**
		 * Updates the text field with the given key.
		 * @param key string keyboard input.
		 */
		update = function(key) {
			var p = getTextCursor(el),
				c = el.value,
				val = '',
				cond = true;
			// Handle keys now
			switch (cond) {
				// Allowed characters
				case (allowed.indexOf(key) !== -1):
					p = p + 1;
					// if text cursor at end
					if (p > format.length) {
						return false;
					}
					// Handle cases where user places cursor before separator
					while (sep.indexOf(c.charAt(p - 1)) !== -1 && p <= format.length) {
						p = p + 1;
					}
					val = c.substr(0, p - 1) + key + c.substr(p);
					// Move csor up a spot if next char is a separator char
					if (allowed.indexOf(c.charAt(p)) === -1
							&& open.indexOf(c.charAt(p)) === -1) {
						p = p + 1;
					}
					break;
				case (key === 'bksp'): // backspace
					p = p - 1;
					// at start of field
					if (p < 0) {
						return false;
					}
					// If previous char is a separator, move a little more
					while (allowed.indexOf(c.charAt(p)) === -1
							&& open.indexOf(c.charAt(p)) === -1
							&& p > 1) {
						p = p - 1;
					}
					val = c.substr(0, p) + format.substr(p, 1) + c.substr(p + 1);
					break;
				case (key === 'del'): // forward delete
					// at end of field
					if (p >= c.length) {
						return false;
					}
					// If next char is a separator and not the end of the text field
					while (sep.indexOf(c.charAt(p)) !== -1
							&& c.charAt(p) !== '') {
						p = p + 1;
					}
					val = c.substr(0, p) + format.substr(p, 1) + c.substr(p + 1);
					p = p + 1; // Move position forward
					break;
				case (key === 'etc'):
					// Catch other allowed chars
					return true;
				default:
					return false; // Ignore the rest
			}
			el.value = ''; // blank it first (Firefox issue)
			el.value = val; // put updated value back in
			setTextCursor(el, p); // Set the text cursor
			return false;
		},
		/**
		 * Returns whether or not a given input is valid for the mask.
		 * @param k string of character to check.
		 * @return bool true if it's a valid character.
		 */
		goodOnes = function(k) {
			// if not in allowed list, or invisible key action
			if (allowed.indexOf(k) === -1 && k !== 'bksp' && k !== 'del' && k !== 'etc') {
				// Need to ensure cursor position not lost
				var p = getTextCursor(el);
				locked = true;
				onbadkey(k);
				// Hold lock long enough for onbadkey function to run
				setTimeout(function() {
					locked = false;
					setTextCursor(el, p);
				}, badwait);
				return false;
			}
			return true;
		},
		/**
		 * Handles the key down events.
		 * @param e Event
		 */
		keyHandlerDown = function(e) {
			if (!enabled) {
				return true;
			}
			if (locked) {
				stopEvent(e);
				return false;
			}
			e = e || event;
			var key = getKey(e);
			// Stop copy and paste
			if ((e.metaKey || e.ctrlKey) && (key === 'X' || key === 'V')) {
				stopEvent(e);
				return false;
			}
			// Allow for OS commands
			if (e.metaKey || e.ctrlKey) {
				return true;
			}
			if (el.value === '') {
				el.value = format;
				setTextCursor(el, 0);
			}
			// Only do update for bksp del
			if (key === 'bksp' || key === 'del') {
				update(key);
				stopEvent(e);
				return false;
			}
			return true;
		},
		/**
		 * Handles the key press events.
		 * @param e Event
		 */
		keyHandlerPress = function(e) {
			if (!enabled) {
				return true;
			}
			if (locked) {
				stopEvent(e);
				return false;
			}
			e = e || event;
			var key = getKey(e);
			// Check if modifier key is being pressed; command
			if (key === 'etc' || e.metaKey || e.ctrlKey || e.altKey) {
				return true;
			}
			if (key !== 'bksp' && key !== 'del' && key !== 'shift') {
				if (!goodOnes(key)) {
					stopEvent(e);
					return false;
				}
				if (update(key)) {
					if (isFilled()) {
						onfilled();
					}
					stopEvent(e, true);
					return true;
				}
				if (isFilled()) {
					onfilled();
				}
				stopEvent(e);
				return false;
			}
			return false;
		},
		/**
		 * Initialize the object.
		 */
		init = function() {
			// Check if an input or textarea tag was passed in
			if (!el.tagName || (el.tagName.toUpperCase() !== 'INPUT'
					&& el.tagName.toUpperCase() !== 'TEXTAREA')) {
				return null;
			}
			// Only place formatted text in field when not preserving
			// text or it's empty.
			if (!preserve || el.value === '') {
				el.value = format;
			}
			// Assign events
			evtAdd(el, 'keydown', function(e) {
				keyHandlerDown(e);
			});
			evtAdd(el, 'keypress', function(e) {
				keyHandlerPress(e);
			});
			// Let us set the initial text state when focused
			evtAdd(el, 'focus', function() {
				startText = el.value;
			});
			// Handle onChange event manually
			evtAdd(el, 'blur', function() {
				if (el.value !== startText && el.onchange) {
					el.onchange();
				}
			});
			return self;
		};

		/**
		 * Resets the text field so just the format is present.
		 */
		self.resetField = function() {
			el.value = format;
		};

		/**
		 * Set the allowed characters that can be used in the mask.
		 * @param a string of characters that can be used.
		 */
		self.setAllowed = function(a) {
			allowed = a;
			self.resetField();
		};

		/**
		 * The format to be used in the mask.
		 * @param f string of the format.
		 */
		self.setFormat = function(f) {
			format = f;
			self.resetField();
		};

		/**
		 * Set the characters to be used as separators.
		 * @param s string representing the separator characters.
		 */
		self.setSeparator = function(s) {
			sep = s;
			self.resetField();
		};

		/**
		 * Set the characters that the user will be typing over.
		 * @param t string representing the characters that will be typed over.
		 */
		self.setTypeon = function(t) {
			open = t;
			self.resetField();
		};

		/**
		 * Sets whether the mask is active.
		 */
		self.setEnabled = function(enable) {
			enabled = enable;
		};

                /**
                 * Local change for Freeside: sets the content of the field,
                 * respecting formatting rules
                 */
		self.setValue = function(value) {
			self.resetField();
			setTextCursor(el, 0);
			var i = 0; // index in value
			while (i < value.length && !isFilled()) {
			  update(value[i]);
			  i++;
			}
		}

		return init();
	};
}(window));