InputMask Class for MooTools
I needed a simple time and date input on a project so I thought about using a mask. I want the user to type "1830" which gets automatically translated to "18:30". While there are two MooTools plugins out there one of them was outdated and I did not like style of the other. So here it his, the MooTools InputMask.
The Script
The script itself is quite simple. It watches what you type into an input field and matches the typed character against a set of rules. You can define a mask like '333' which allows you to input '033', '122' but not '555'. In addition to that you can also define complex rules for values like time and date: in the time example you can input everything from 01 to 23 but everything above will be blocked. Please note that the other mask plugins I've tried did not have this sort of functionality. They both allowed me to input '29' because the 9 is needed for '09' and '19'. I'm not saying that my script will always generate valid input, please validate input data on the server side!
The code is very clean and elegant. You can easily define global rules via InputMask.defineRule/s, local rules via the "rules" option and of course you are able to customize the mask for every instance.
/*
Script: InputMask.js
Allows masking of input elements
License:
MIT-style license.
Authors:
Christoph Pojer
*/
(function(){
var InputMask = this.InputMask = new Class({
Implements: [Options, Events],
options: {
/*onKeypress: $empty,
onError: $empty*/
rules: {},
mask: null
},
initialize: function(selector, options){
this.setOptions(options);
this.rules = $merge(this.options.rules, InputMask.lookupRules());
this.keys = Hash.getKeys(this.rules);
var self = this;
this.fire = function(e){
self.fireEvent(e.type)[e.type].apply(self, [e, this]);
};
this.attach(selector);
},
attach: function(selector){
$$(selector).addEvents({
keypress: this.fire
});
return this;
},
detach: function(selector){
$$(selector).removeEvents({
keypress: this.fire
});
return this;
},
keypress: function(e, element){
var key = e.key.toLowerCase(), value = element.get('value');
if (e.control || e.meta) return (e.key == 'a' || e.key == 'c');
var range = element.getSelectedRange();
if ($chk(range.start) && range.start != range.end){
e.stop();
element.set('value', value = value.substring(0, range.start) + value.substring(range.end, value.length));
} else if (key == 'backspace'){
e.stop();
element.set('value', value.substring(0, value.length - 1));
this.previous(element);
}
// Fixes weird keypress bug for % & / ( ' .
if ((['right', 'up', 'down', 'left'].contains(key) && e.shift) || key == 'delete') e.stop();
if (key.length > 1) return true
var before = value;
this.next(element);
value = '' + element.get('value');
e.stop();
var current = this.options.mask.charAt(value.length),
group = this.getPrevious(value, key);
for (var i in this.rules){
var rule = this.rules[i],
result = ($type(rule) == 'function' ? rule(key, {
element: element,
value: value,
position: value.length,
group: group
}) : key.test(rule));
if (current == i && result){
element.set('value', value + key);
return true;
}
}
this.next(element);
if (before == element.get('value')) this.fireEvent('error', [element, key]);
},
getPrevious: function(value, key){
var length = value.length, mask = this.options.mask;
if (!value) return key;
var group = [key], current = mask.charAt(length);
for (var i = length; i--; ){
if (mask.charAt(i) == current) group.push(value.charAt(i));
else break;
}
return group.reverse().join('');
},
previous: function(element){
var value = element.get('value'), length = value.length - 1, mask = this.options.mask;
if (!value) return this;
for (var i = length; i--; ){
if (this.keys.contains(mask.charAt(length))) break;
else element.set('value', value.substring(0, length));
}
return this;
},
next: function(element){
var value = element.get('value'), length = value.length, mask = this.options.mask;
if (mask.length <= length) return this;
for (var i = length; i <= mask.length; i++){
var current = mask.charAt(length);
if (this.keys.contains(current)) break;
else element.set('value', value + current);
}
return this;
}
});
InputMask.extend({
rules: {},
defineRule: function(rule, chars){
this.rules[rule] = chars;
return this;
},
defineRules: function(rules){
for (var i in rules) this.defineRule(i, rules[i]);
return this;
},
lookupRule: function(rule){
return rules[rule] || null;
},
lookupRules: function(rules){
if (!rules) return this.rules;
var result = {};
rules.each(function(rule){
result[rule] = this.rules[rule] || null;
}, this);
return result;
},
upTo: function(max){
max = '' + max;
return function(key, options){
if (!options.group.test(/^\d+$/)) return false;
return options.group <= max.substr(0, options.group.length);
};
}
}).defineRules((function(){
var rules = {
'0': /0/,
a: /\w/,
x: /(\w|\d)/
};
for (var i = 1; i <= 9; i++) rules[i] = new RegExp('[0-' + i + ']');
return rules;
})());
InputMask.Time = new Class({
Extends: InputMask,
options: {
rules: {
h: InputMask.upTo(23)
},
mask: 'hh:59'
}
});
InputMask.Date = new Class({
Extends: InputMask,
options: {
rules: {
m: InputMask.upTo(12),
d: InputMask.upTo(31)
},
mask: 'mm/dd/2999'
}
});
})();
Usage
Using the class is very simple. Just instantiate it in domready and pass a selector, a collection of elements (via $$) or one element (via document.id/$).
new InputMask(selector, {
mask: '(999) 999 999 - 99'
});
Download
The script is very small, with only 4.2kb in size, that is 1/4 the size of the others. In addition all you need is the MooTools-More Script "Element.Forms" and you are good to go.
Special thanks go out to Scott Kyle (@appden), one of the MooTools core developers. He helped me increase the code quality and had great suggestions. He is an amazing programmer and he is a "code bitch" like me. 
If you find any bugs please fee free to post them in the comments. As with any code dealing with keyboard-events they are messed up across different browsers.
Update August 27
- onError event fires when a user types an invalid key
- Fix for various special chars that got through as numbers
Leave a comment
19 comments on InputMask Class for MooTools
dlh posted on 25th January 2010
davidck posted on 30th October 2009
The problem with this is when I type in 1000, I expect 1,000. Instead, I get 100,0. Which makes sense, but is there a way to make the pattern matching reversable?
Adriaan Nel posted on 27th October 2009
Diego posted on 20th October 2009
Very nice work. Really.
Can I ask U some help?
It's possible to define a mask for reals, without set a string lenght?
Tks.
(Sorry for my english)
rev087 posted on 25th September 2009
One of the main issues with all the javascript input masks I've tested so far, is that you sacrifice a lot of default functionality that the user expects.
I'm aware that the code should stay small and simple, but would it be possible to add support for moving the cursor back and forth with the arrow keys (and then deleting/backspacing)? The script could add a space in the removed character. Same for selecting while holding shift, and using the Home and End keys instead of arrows.
Again, this feature suggestion might be "overkill", and maybe not what you planned for the script, but I'd like to see the mask characters (like " / / " in the fields while they're empty. When I type on empty date/time fields on forms, I'm always unsure if I should type the "/" and ":" or there will be a surprise input mask.
Anyways, congrats for the script, seems really nicely coded. I've started some input mask scripts in the past myself, but gave up every time after thinking about all those features and failing to implementing them all in a nice, bug-free way -.-
ps.: nice shirt =D
Chris posted on 28th August 2009
Robert posted on 28th August 2009
But I think something is broken:
When I try to put 02/30/1991 into a date, it only allows me 02/30/19 and then nothing. 02/30/1881 or 02/30/2001 is ok.
thanks,
rt
Aaron Newton posted on 27th August 2009
Chris posted on 27th August 2009
In addition to that I have also added an onError event which highlights the input field when you type something wrong.
Fábio M. Costa posted on 27th August 2009
I liked the way you did the fireEvent of the key events. Super compact.
I don't like your style too. JK. Not.
Gordon Tucker posted on 26th August 2009
One small thing though. It allows you to type '/'. So if someone (like me) still types "10/10/09" (mostly out of habit), it will display 10//1/00 then stop.
Sean McArthur posted on 26th August 2009
Delete at "10:" should leave me with "1", since I've only enter 2 characters, it feels weird to have to delete multiple times if I meant to type 11 or 12.
Aaron Newton posted on 26th August 2009
Chris posted on 26th August 2009
Fabio: No offence here, but it is a jQuery port of your plugin and it is quite old already ;) And also, I usually don't like the code of most other people.
Chris Drackett: Good point. I need to come up with a clean solution for that though.
I just noticed one critical problem - when you tab through a field - it gets cleared. This is not what one would expect, and will lead to loss of pre-entered data.
I've fixed my copy by adding (at least it seems to work):
if (e.key == 'tab') return true;
to the second line of keypress: function(e, element)
It would be nice to have a rangeOf() along with upTo() this would solve a date: 00/00/00000 entry problem. Although I see how it would be tricky to fit into this framework.
Thanks
Dave