InputMask Class for MooTools

filed under MooTools, posted on 27th August 2009 by Chris, 20 comments

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.

 

Demo

 


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.

 


Download this file

/*
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.

 

Download this script

 

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

20 comments on InputMask Class for MooTools

STB posted on 23rd February 2010

Wery useful plugin. Thanks for demo.

dlh posted on 25th January 2010

This is a great script. I also would like to see support for currency or reals. Something like d*.99 for a format would be great. Another thought would be to allow entry of the '.' (in this case) and move the insertion point to after the period. This would also allow one to define a mask like 999999.99 and enter keys 1.25 or to enter a date using character sequences: 1/9/2010 (which might then be formated to the mask).

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

davidck posted on 30th October 2009

Hi, thanks, this script seems useful. I'm having an issue though. I'm making a simple rule that does currency. I have something like this as rule: '999,999'

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

This is very handy, thanks, I'll definitely be using it..., but now after reading REV087's post below, I agree with him...

Diego posted on 20th October 2009

Hi.
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

(caution: bad engrish follows)

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

Dammit :D I introduced this with yesterday's update - fixed now. Thanks for reporting

Robert posted on 28th August 2009

Hey, nice plugin.

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

Much better. I'm still not crazy about the pattern, but this is the most usable behavior I've yet seen.

Chris posted on 27th August 2009

Thank you for your comments so far. I've tried to fix that (ugly) bug where it allowed / in the date input. Apparently when doing a string comparison '/' <= '12' is true, didn't really think about that (Damn you Scott, you told me I don't need to convert to integer! ;P Anyway, it is my fault not converting the upTo value back to a number). I have added an additional check for the upTo method that checks if the field only contains numbers.

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 found out theres no way to make a bug-free inputmask plugin with some crazy, unreadable, code. People will always find a bug and sometimes you'll have to change your code in an ugly way to fix the bug.

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

Looks nice. I agree with Aaron about the masks making me think my keyboard was broken. Tried to type "4:30" instead of "04:30" and I thought I was going crazy :)

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

I guess I meant that when deleting a character I didn't enter, it should delete two characters.

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

My issue with input masks is that they break the interface. If I put my cursor in an input and start typing, and nothing happens, I start looking to see if my keyboard is working. I think these masks should, instead, display values that are not valid in red, or display a message at least when I hit a key that isn't valid. If I'm supposed to put in "12:30" and I put in "12:30pm" and nothing happens when I hit "p", it's absolutely not clear that this is prevented by YOUR design and not by some other problem; my own error (my keyboard or something - is my "p" key not working???), something weird with my browser, who knows.

Chris posted on 26th August 2009

Sean: isn't that what it already does? ;)

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.
2 http://cpojer.net/blog/InputMask_Class_for_MooTools#comments 15
Page 1 of 2
Styx PHP Framework Contact Follow me on Twitter