﻿/*
Written by Adam M. Euans
(Initally written by digitalbush.com)
Date: 2008/01/09

Overview:
This is a masked input plugin for the initally for jQuery, but ported to MooTools javascript library. It allows a user to more easily enter fixed width input where you would like them to enter the data in a certain format (dates,phone numbers, etc). It has been tested on Internet Explorer 6/7, Firefox 1.5/2, Safari, and Opera.

A mask is defined by a format made up of mask and placeholder characters. Any character not in the placeholder character list below is considered a mask character. Mask characters will be automatically entered for the user as they type and will not be able to be removed by the user.

The following placeholder characters are predefined:
* a - Represents an alpha character (A-Z,a-z)
* 9 - Represents a numeric character (0-9)
* * - Represents an alphanumeric character (A-Z,a-z,0-9) 
	
Functions:
addPlaceholder	: Add additional regular expression place holders. Default = a, 9, *
mask			: Function that sets the mask
inlineMask		: Function that will look for inline masks

Options:
placeholder		: Update the place holder.  Default = "_"		
destroyInvalid	: Clear the input box if the input value doesn't match mask
stripMask		: Removes the mask from the valid input
onComplete		: What to do after the mask is complete
	
Inline Options:
mask			: Mask to apply
maskSettings	: Options to apply to mask

Usage:
<script src="mooMask.js" type="text/javascript"></script>
<script type="text/javascript">
<!--
mooMask = new mooMask();
mooMask.mask($('inputField'), '(999) 999-9999');

- or - 

mooMask = new mooMask();
mooMask.inlineMask();
//-->
</script>
<form>
<input name='test1' id='test1' mask='(999) 999-9999' maskOptions='{placeholder:'*'}'
</form>

*/
var mooMask = new Class({
    initialize: function(options) {
        $extend(this.options, options);
    },

    addPlaceholder: function(c, r) {
        this.options.charMap[c] = r;
    },

    options: {
        onComplete: null,
        placeholder: "_",
        destroyInvalid: true,
        stripMask: false,
        charMap: {
            '9': "[0-9]",
            'a': "[A-Za-z]",
            '*': "[A-Za-z0-9]"
        }
    },

    inlineMask: function() {
        $$('INPUT[mask]').each(function(el) {
            this.mask(el, el.getProperty('mask'), Json.evaluate(el.getProperty('maskOptions')));
        }, this);
    },

    mask: function(el, mask, options) {
        var el = ($type(el) == 'array') ? el : [$(el)];
        el.each(function(ele) {
            this._mask(ele, mask, options)
        }, this);
    },

    _mask: function(el, mask, options) {
        var options = $extend({
            onComplete: this.options.onComplete,
            placeholder: this.options.placeholder,
            destroyInvalid: this.options.destroyInvalid,
            stripMask: this.options.stripMask
        }, options);

        var charMap = this.options.charMap;

        //Build Regex for format validation
        var reString = "^";
        for (var i = 0; i < mask.length; i++)
            reString += (charMap[mask.charAt(i)] || ("\\" + mask.charAt(i)));
        reString += "$";
        var re = new RegExp(reString);

        //return this.each(function(){		
        var input = $(el);
        var buffer = new Array(mask.length);
        var locked = new Array(mask.length);

        //Build buffer layout from mask
        for (var i = 0; i < mask.length; i++) {
            locked[i] = charMap[mask.charAt(i)] == null;
            buffer[i] = locked[i] ? mask.charAt(i) : options.placeholder;
        };
        var msk = buffer.join('');

        //Event Bindings
        input.addEvent('focus', function() {
            checkVal();
            writeBuffer();
            setCaretPosition(el, 0);
        });

        input.addEvent('blur', checkVal);

        //Paste events for IE and Mozilla thanks to Kristinn Sigmundsson
        if (window.msie)
            input.onpaste = function() { setTimeout(checkVal, 0); };
        else if (window.gecko)
            input.addEvent('input', checkVal, false);

        var ignore = false;  //Variable for ignoring control keys

        input.addEvent('keydown', function(e) {
            var pos = getCaretPosition(this);
            var e = new Event(e);
            var k = e.code;
            ignore = (k < 16 || (k > 16 && k < 32) || (k > 32 && k < 41));
            //delete selection before proceeding
            if ((pos.begin - pos.end) != 0 && (!ignore || k == 8 || k == 46)) {
                clearBuffer(pos.begin, pos.end);
            }
            //backspace and delete get special treatment
            if (k == 8) {//backspace
                while (pos.begin-- >= 0) {
                    if (!locked[pos.begin]) {
                        buffer[pos.begin] = options.placeholder;
                        if (window.opera) {
                            //Opera won't let you cancel the backspace, so we'll let it backspace over a dummy character.								
                            writeBuffer(pos.begin);
                            setCaretPosition(el, pos.begin + 1);
                        } else {
                            writeBuffer();
                            setCaretPosition(el, pos.begin);
                        }
                        break;
                    }
                }
                e.stop();
            } else if (k == 46) {//delete
                clearBuffer(pos.begin, pos.begin + 1);
                writeBuffer();
                setCaretPosition(el, pos.begin);
                e.stop();
            } else if (k == 27) {
                clearBuffer(0, mask.length);
                writeBuffer();
                setCaretPosition(el, 0);
                e.stop();
            }

        });

        input.addEvent('keypress', function(e) {
            if (ignore) {
                ignore = false;
                return;
            }

            e = new Event(e);
            var k = e.code;

            var pos = getCaretPosition(el);
            var caretPos = pos.begin;

            if (e.control || e.alt) {//Ignore
                return true;
            } else if ((k >= 41 && k <= 122) || k == 32 || k > 186) {//typeable characters
                while (pos.begin < mask.length) {
                    var reString = charMap[mask.charAt(pos.begin)];
                    var match;
                    if (reString) {
                        var reChar = new RegExp(reString);
                        match = String.fromCharCode(k).match(reChar);
                    } else {//we're on a mask char, go forward and try again
                        pos.begin += 1;
                        pos.end = pos.begin;
                        caretPos += 1;
                        continue;
                    }

                    if (match)
                        buffer[pos.begin] = String.fromCharCode(k);
                    else {
                        caretPos -= 1;
                        e.stop();
                    }

                    while (++caretPos < mask.length) {//seek forward to next typable position
                        if (!locked[caretPos])
                            break;
                    }
                    break;
                }
            } else
                e.stop();

            writeBuffer();
            if (options.onComplete && caretPos >= buffer.length)
                options.onComplete.call(input);
            else
                setCaretPosition(el, caretPos);

            e.stop();
        });

        //Helper Methods
        function clearBuffer(start, end) {
            for (var i = start; i < end; i++) {
                if (!locked[i])
                    buffer[i] = options.placeholder;
            }
        };

        function writeBuffer(pos) {
            var s = "";
            for (var i = 0; i < mask.length; i++) {
                s += buffer[i];
                if (i == pos)
                    s += options.placeholder;
            }
            input.value = s;
            return s;
        };

        function checkVal() {
            //try to place charcters where they belong
            var test = input.value;
            var pos = 0;
            for (var i = 0; i < mask.length; i++) {
                if (!locked[i]) {
                    while (pos++ < test.length) {
                        //Regex Test each char here.
                        var reChar = new RegExp(charMap[mask.charAt(i)]);
                        if (test.charAt(pos - 1).match(reChar)) {
                            buffer[i] = test.charAt(pos - 1);
                            break;
                        }
                    }
                }
            }
            var s = writeBuffer();
            if (!s.match(re)) {
                //console.log(stripMask())
                if (options.destroyInvalid || (!options.destroyInvalid && (msk == test))) {
                    input.value = "";
                    clearBuffer(0, mask.length);
                }
            } else if (options.stripMask) {
                input.value = stripMask();
            }
        };

        function stripMask() {
            var r = '';
            var s = input.value;
            for (i = 0; i < locked.length; i++) {
                if (!locked[i]) { r += s.charAt(i); }
            }
            return r
        };

        //Helper Functions for Caret positioning
        function getCaretPosition(ctl) {
            var res = { begin: 0, end: 0 };
            if (ctl.setSelectionRange) {
                res.begin = ctl.selectionStart;
                res.end = ctl.selectionEnd;
            } else if (document.selection && document.selection.createRange) {
                var range = document.selection.createRange();
                res.begin = 0 - range.duplicate().moveStart('character', -100000);
                res.end = res.begin + range.text.length;
            }
            return res;
        };

        function setCaretPosition(ctl, pos) {
            if (ctl.setSelectionRange) {
                ctl.focus();
                ctl.setSelectionRange(pos, pos);
            } else if (ctl.createTextRange) {
                var range = ctl.createTextRange();
                range.collapse(true);
                range.moveEnd('character', pos);
                range.moveStart('character', pos);
                range.select();
            }
        };
    }
});
