/*! jQuery UI Virtual Keyboard v1.25.29 *//*
 Author: Jeremy Satterfield
 Maintained: Rob Garrison (Mottie on github)
 Licensed under the MIT License

 An on-screen virtual keyboard embedded within the browser window which
 will popup when a specified entry field is focused. The user can then
 type and preview their input before Accepting or Canceling.

 This plugin adds default class names to match jQuery UI theme styling.
 Bootstrap & custom themes may also be applied - See
 https://github.com/Mottie/Keyboard#themes

 Requires:
 jQuery v1.4.3+
 Caret plugin (included)
 Optional:
 jQuery UI (position utility only) & CSS theme
 jQuery mousewheel

 Setup/Usage:
 Please refer to https://github.com/Mottie/Keyboard/wiki

 -----------------------------------------
 Caret code modified from jquery.caret.1.02.js
 Licensed under the MIT License:
 http://www.opensource.org/licenses/mit-license.php
 -----------------------------------------
 */
/*jshint browser:true, jquery:true, unused:false */
/*global require:false, define:false, module:false */
;(function (factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery'], factory);
    } else if (typeof module === 'object' && typeof module.exports === 'object') {
        module.exports = factory(require('jquery'));
    } else {
        factory(jQuery);
    }
}(function ($) {
    'use strict';
    var $keyboard = $.keyboard = function (el, options) {
        var o, base = this;

        base.version = '1.25.29';

        // Access to jQuery and DOM versions of element
        base.$el = $(el);
        base.el = el;

        // Add a reverse reference to the DOM object
        base.$el.data('keyboard', base);

        base.init = function () {
            var k, position, tmp,
                kbcss = $keyboard.css,
                kbevents = $keyboard.events;
            base.settings = options || {};
            // shallow copy position to prevent performance issues; see #357
            if (options && options.position) {
                position = $.extend({}, options.position);
                options.position = null;
            }
            base.options = o = $.extend(true, {}, $keyboard.defaultOptions, options);
            if (position) {
                o.position = position;
                options.position = position;
            }

            // keyboard is active (not destroyed);
            base.el.active = true;
            // unique keyboard namespace
            base.namespace = '.keyboard' + Math.random().toString(16).slice(2);
            // extension namespaces added here (to unbind listeners on base.$el upon destroy)
            base.extensionNamespace = [];
            // Shift and Alt key toggles, sets is true if a layout has more than one keyset
            // used for mousewheel message
            base.shiftActive = base.altActive = base.metaActive = base.sets = base.capsLock = false;
            // Class names of the basic key set - meta keysets are handled by the keyname
            base.rows = ['', '-shift', '-alt', '-alt-shift'];

            base.inPlaceholder = base.$el.attr('placeholder') || '';
            // html 5 placeholder/watermark
            base.watermark = $keyboard.watermark && base.inPlaceholder !== '';
            // convert mouse repeater rate (characters per second) into a time in milliseconds.
            base.repeatTime = 1000 / (o.repeatRate || 20);
            // delay in ms to prevent mousedown & touchstart from both firing events at the same time
            o.preventDoubleEventTime = o.preventDoubleEventTime || 100;
            // flag indication that a keyboard is open
            base.isOpen = false;
            // is mousewheel plugin loaded?
            base.wheel = $.isFunction($.fn.mousewheel);
            // special character in regex that need to be escaped
            base.escapeRegex = /[-\/\\^$*+?.()|[\]{}]/g;

            // keyCode of keys always allowed to be typed
            k = $keyboard.keyCodes;
            // base.alwaysAllowed = [20,33,34,35,36,37,38,39,40,45,46];
            base.alwaysAllowed = [
                k.capsLock,
                k.pageUp,
                k.pageDown,
                k.end,
                k.home,
                k.left,
                k.up,
                k.right,
                k.down,
                k.insert,
                k.delete
            ];
            base.$keyboard = [];
            // keyboard enabled; set to false on destroy
            base.enabled = true;
            // make a copy of the original keyboard position
            if (!$.isEmptyObject(o.position)) {
                o.position.orig_at = o.position.at;
            }

            base.checkCaret = (o.lockInput || $keyboard.checkCaretSupport());

            base.last = {
                start: 0,
                end: 0,
                key: '',
                val: '',
                preVal: '',
                layout: '',
                virtual: true,
                keyset: [false, false, false], // [shift, alt, meta]
                wheel_$Keys: null,
                wheelIndex: 0,
                wheelLayers: []
            };
            // used when building the keyboard - [keyset element, row, index]
            base.temp = ['', 0, 0];

            // Callbacks
            $.each([
                kbevents.kbInit,
                kbevents.kbBeforeVisible,
                kbevents.kbVisible,
                kbevents.kbHidden,
                kbevents.inputCanceled,
                kbevents.inputAccepted,
                kbevents.kbBeforeClose
            ], function (i, callback) {
                if ($.isFunction(o[callback])) {
                    // bind callback functions within options to triggered events
                    base.$el.bind(callback + base.namespace + 'callbacks', o[callback]);
                }
            });

            // Close with esc key & clicking outside
            if (o.alwaysOpen) {
                o.stayOpen = true;
            }

            tmp = $(document);
            if (base.el.ownerDocument !== document) {
                tmp = tmp.add(base.el.ownerDocument);
            }
            tmp.bind('mousedown keyup touchstart checkkeyboard '.split(' ')
                .join(base.namespace + ' '), base.checkClose);

            // Display keyboard on focus
            base.$el
                .addClass(kbcss.input + ' ' + o.css.input)
                .attr({
                    'aria-haspopup': 'true',
                    'role': 'textbox'
                });

            // set lockInput if the element is readonly; or make the element readonly if lockInput is set
            if (o.lockInput || base.el.readOnly) {
                o.lockInput = true;
                base.$el
                    .addClass(kbcss.locked)
                    .attr({
                        'readonly': 'readonly'
                    });
            }
            // add disabled/readonly class - dynamically updated on reveal
            if (base.$el.is(':disabled') || (base.$el.attr('readonly') &&
                !base.$el.hasClass(kbcss.locked))) {
                base.$el.addClass(kbcss.noKeyboard);
            }

            if (o.openOn) {
                base.bindFocus();
            }

            // Add placeholder if not supported by the browser
            if (!base.watermark && base.$el.val() === '' && base.inPlaceholder !== '' &&
                base.$el.attr('placeholder') !== '') {
                base.$el
                    .addClass(kbcss.placeholder) // css watermark style (darker text)
                    .val(base.inPlaceholder);
            }

            base.$el.trigger(kbevents.kbInit, [base, base.el]);

            // initialized with keyboard open
            if (o.alwaysOpen) {
                base.reveal();
            }

        };

        base.toggle = function () {
            var $toggle = base.$keyboard.find('.' + $keyboard.css.keyToggle),
                locked = !base.enabled;
            // prevent physical keyboard from working
            base.$preview.prop('readonly', locked || base.options.lockInput);
            // disable all buttons
            base.$keyboard
                .toggleClass($keyboard.css.keyDisabled, locked)
                .find('.' + $keyboard.css.keyButton)
                .not($toggle)
                .prop('disabled', locked)
                .attr('aria-disabled', locked);
            $toggle.toggleClass($keyboard.css.keyDisabled, locked);
            // stop auto typing
            if (locked && base.typing_options) {
                base.typing_options.text = '';
            }
        };

        base.setCurrent = function () {
            var kbcss = $keyboard.css,
            // close any "isCurrent" keyboard (just in case they are always open)
                $current = $('.' + kbcss.isCurrent),
                kb = $current.data('keyboard');
            // close keyboard, if not self
            if (!$.isEmptyObject(kb) && kb.el !== base.el) {
                kb.close(kb.options.autoAccept ? 'true' : false);
            }
            $current.removeClass(kbcss.isCurrent);
            // ui-keyboard-has-focus is applied in case multiple keyboards have
            // alwaysOpen = true and are stacked
            $('.' + kbcss.hasFocus).removeClass(kbcss.hasFocus);

            base.$el.addClass(kbcss.isCurrent);
            base.$keyboard.addClass(kbcss.hasFocus);
            base.isCurrent(true);
            base.isOpen = true;
        };

        base.isCurrent = function (set) {
            var cur = $keyboard.currentKeyboard || false;
            if (set) {
                cur = $keyboard.currentKeyboard = base.el;
            } else if (set === false && cur === base.el) {
                cur = $keyboard.currentKeyboard = '';
            }
            return cur === base.el;
        };

        base.isVisible = function () {
            return base.$keyboard && base.$keyboard.length ? base.$keyboard.is(':visible') : false;
        };

        base.focusOn = function () {
            if (!base && base.el.active) {
                // keyboard was destroyed
                return;
            }
            if (!base.isVisible()) {
                clearTimeout(base.timer);
                base.reveal();
            }
        };

        // add redraw method to make API more clear
        base.redraw = function () {
            // update keyboard after a layout change
            if (base.$keyboard.length) {

                base.last.preVal = '' + base.last.val;
                base.last.val = base.$preview && base.$preview.val() || base.$el.val();
                base.$el.val( base.last.val );

                base.removeKeyboard();
                base.shiftActive = base.altActive = base.metaActive = false;
            }
            base.isOpen = o.alwaysOpen;
            base.reveal(true);
        };

        base.reveal = function (redraw) {
            var alreadyOpen = base.isOpen,
                kbcss = $keyboard.css;
            base.opening = !alreadyOpen;
            // remove all 'extra' keyboards by calling close function
            $('.' + kbcss.keyboard).not('.' + kbcss.alwaysOpen).each(function(){
                var kb = $(this).data('keyboard');
                if (!$.isEmptyObject(kb)) {
                    kb.close(kb.options.autoAccept && kb.options.autoAcceptOnEsc ? 'true' : false);
                }
            });

            // Don't open if disabled
            if (base.$el.is(':disabled') || (base.$el.attr('readonly') && !base.$el.hasClass(kbcss.locked))) {
                base.$el.addClass(kbcss.noKeyboard);
                return;
            } else {
                base.$el.removeClass(kbcss.noKeyboard);
            }

            // Unbind focus to prevent recursion - openOn may be empty if keyboard is opened externally
            if (o.openOn) {
                base.$el.unbind($.trim((o.openOn + ' ').split(/\s+/).join(base.namespace + ' ')));
            }

            // build keyboard if it doesn't exist; or attach keyboard if it was removed, but not cleared
            if (!base.$keyboard || base.$keyboard &&
                (!base.$keyboard.length || $.contains(document.body, base.$keyboard[0]))) {
                base.startup();
            }

            // clear watermark
            if (!base.watermark && base.el.value === base.inPlaceholder) {
                base.$el
                    .removeClass(kbcss.placeholder)
                    .val('');
            }
            // save starting content, in case we cancel
            base.originalContent = base.$el.val();
            base.$preview.val(base.originalContent);

            // disable/enable accept button
            if (o.acceptValid) {
                base.checkValid();
            }

            if (o.resetDefault) {
                base.shiftActive = base.altActive = base.metaActive = false;
            }
            base.showSet();

            // beforeVisible event
            if (!base.isVisible()) {
                base.$el.trigger($keyboard.events.kbBeforeVisible, [base, base.el]);
            }
            base.setCurrent();
            // update keyboard - enabled or disabled?
            base.toggle();

            // show keyboard
            base.$keyboard.show();

            // adjust keyboard preview window width - save width so IE won't keep expanding (fix issue #6)
            if (o.usePreview && $keyboard.msie) {
                if (typeof base.width === 'undefined') {
                    base.$preview.hide(); // preview is 100% browser width in IE7, so hide the damn thing
                    base.width = Math.ceil(base.$keyboard.width()); // set input width to match the widest keyboard row
                    base.$preview.show();
                }
                base.$preview.width(base.width);
            }

            base.position = $.isEmptyObject(o.position) ? false : o.position;

            // position after keyboard is visible (required for UI position utility) and appropriately sized
            if ($.ui && $.ui.position && base.position) {
                // get single target position || target stored in element data (multiple targets) || default @ element
                base.position.of = base.position.of || base.$el.data('keyboardPosition') || base.$el;
                base.position.collision = base.position.collision || 'flipfit flipfit';
                o.position.at = o.usePreview ? o.position.orig_at : o.position.at2;
                base.$keyboard.position(base.position);
            }

            base.checkDecimal();

            // get preview area line height
            // add roughly 4px to get line height from font height, works well for font-sizes from 14-36px
            // needed for textareas
            base.lineHeight = parseInt(base.$preview.css('lineHeight'), 10) ||
                parseInt(base.$preview.css('font-size'), 10) + 4;

            if (o.caretToEnd) {
                base.saveCaret(base.originalContent.length, base.originalContent.length);
            }

            // IE caret haxx0rs
            if ($keyboard.allie) {
                // sometimes end = 0 while start is > 0
                if (base.last.end === 0 && base.last.start > 0) {
                    base.last.end = base.last.start;
                }
                // IE will have start -1, end of 0 when not focused (see demo: https://jsfiddle.net/Mottie/fgryQ/3/)
                if (base.last.start < 0) {
                    // ensure caret is at the end of the text (needed for IE)
                    base.last.start = base.last.end = base.originalContent.length;
                }
            }

            if (alreadyOpen || redraw) {
                // restore caret position (userClosed)
                $keyboard.caret(base.$preview, base.last);
                return base;
            }

            // opening keyboard flag; delay allows switching between keyboards without immediately closing
            // the keyboard
            base.timer2 = setTimeout(function () {
                var undef;
                base.opening = false;
                // Number inputs don't support selectionStart and selectionEnd
                // Number/email inputs don't support selectionStart and selectionEnd
                if (!/(number|email)/i.test(base.el.type) && !o.caretToEnd) {
                    // caret position is always 0,0 in webkit; and nothing is focused at this point... odd
                    // save caret position in the input to transfer it to the preview
                    // inside delay to get correct caret position
                    base.saveCaret(undef, undef, base.$el);
                }
                if (o.initialFocus) {
                    $keyboard.caret(base.$preview, base.last);
                }
                // save event time for keyboards with stayOpen: true
                base.last.eventTime = new Date().getTime();
                base.$el.trigger($keyboard.events.kbVisible, [base, base.el]);
                base.timer = setTimeout(function () {
                    // get updated caret information after visible event - fixes #331
                    if (base) { // Check if base exists, this is a case when destroy is called, before timers fire
                        base.saveCaret();
                    }
                }, 200);
            }, 10);
            // return base to allow chaining in typing extension
            return base;
        };

        base.updateLanguage = function () {
            // change language if layout is named something like 'french-azerty-1'
            var layouts = $keyboard.layouts,
                lang = o.language || layouts[o.layout] && layouts[o.layout].lang &&
                    layouts[o.layout].lang || [o.language || 'en'],
                kblang = $keyboard.language;

            // some languages include a dash, e.g. 'en-gb' or 'fr-ca'
            // allow o.language to be a string or array...
            // array is for future expansion where a layout can be set for multiple languages
            lang = ($.isArray(lang) ? lang[0] : lang).split('-')[0];

            // set keyboard language
            o.display = $.extend(true, {},
                kblang.en.display,
                kblang[lang] && kblang[lang].display || {},
                base.settings.display
            );
            o.combos = $.extend(true, {},
                kblang.en.combos,
                kblang[lang] && kblang[lang].combos || {},
                base.settings.combos
            );
            o.wheelMessage = kblang[lang] && kblang[lang].wheelMessage || kblang.en.wheelMessage;
            // rtl can be in the layout or in the language definition; defaults to false
            o.rtl = layouts[o.layout] && layouts[o.layout].rtl || kblang[lang] && kblang[lang].rtl || false;

            // save default regex (in case loading another layout changes it)
            base.regex = kblang[lang] && kblang[lang].comboRegex || $keyboard.comboRegex;
            // determine if US '.' or European ',' system being used
            base.decimal = /^\./.test(o.display.dec);
            base.$el
                .toggleClass('rtl', o.rtl)
                .css('direction', o.rtl ? 'rtl' : '');
        };

        base.startup = function () {
            var kbcss = $keyboard.css;
            // ensure base.$preview is defined; but don't overwrite it if keyboard is always visible
            if (!((o.alwaysOpen || o.userClosed) && base.$preview)) {
                base.makePreview();
            }
            if (!(base.$keyboard && base.$keyboard.length)) {
                // custom layout - create a unique layout name based on the hash
                if (o.layout === 'custom') {
                    o.layoutHash = 'custom' + base.customHash();
                }
                base.layout = o.layout === 'custom' ? o.layoutHash : o.layout;
                base.last.layout = base.layout;

                base.updateLanguage();
                if (typeof $keyboard.builtLayouts[base.layout] === 'undefined') {
                    if ($.isFunction(o.create)) {
                        // create must call buildKeyboard() function; or create it's own keyboard
                        base.$keyboard = o.create(base);
                    } else if (!base.$keyboard.length) {
                        base.buildKeyboard(base.layout, true);
                    }
                }
                base.$keyboard = $keyboard.builtLayouts[base.layout].$keyboard.clone();
                base.$keyboard.data('keyboard', base);
                if ((base.el.id || '') !== '') {
                    // add ID to keyboard for styling purposes
                    base.$keyboard.attr('id', base.el.id + $keyboard.css.idSuffix);
                }

                base.makePreview();
                // build preview display
                if (o.usePreview) {
                    // restore original positioning (in case usePreview option is altered)
                    if (!$.isEmptyObject(o.position)) {
                        o.position.at = o.position.orig_at;
                    }
                } else {
                    // No preview display, use element and reposition the keyboard under it.
                    if (!$.isEmptyObject(o.position)) {
                        o.position.at = o.position.at2;
                    }
                }

            }

            base.$decBtn = base.$keyboard.find('.' + kbcss.keyPrefix + 'dec');
            // add enter to allowed keys; fixes #190
            if (o.enterNavigation || base.el.nodeName === 'TEXTAREA') {
                base.alwaysAllowed.push(13);
            }

            base.bindKeyboard();

            base.$keyboard.appendTo(o.appendLocally ? base.$el.parent() : o.appendTo || 'body');

            base.bindKeys();

            // adjust with window resize; don't check base.position
            // here in case it is changed dynamically
            if (o.reposition && $.ui && $.ui.position && o.appendTo == 'body') {
                $(window).bind('resize' + base.namespace, function () {
                    if (base.position && base.isVisible()) {
                        base.$keyboard.position(base.position);
                    }
                });
            }

        };

        base.makePreview = function () {
            if (o.usePreview) {
                var indx, attrs, attr, removedAttr,
                    kbcss = $keyboard.css;
                base.$preview = base.$el.clone(false)
                    .data('keyboard', base)
                    .removeClass(kbcss.placeholder + ' ' + kbcss.input)
                    .addClass(kbcss.preview + ' ' + o.css.input)
                    .attr('tabindex', '-1')
                    .show(); // for hidden inputs
                base.preview = base.$preview[0];

                // Switch the number input field to text so the caret positioning will work again
                if (base.preview.type === 'number') {
                    base.preview.type = 'text';
                }

                // remove extraneous attributes.
                removedAttr = /^(data-|id|aria-haspopup)/i;
                attrs = base.$preview.get(0).attributes;
                for (indx = attrs.length - 1; indx >= 0; indx--) {
                    attr = attrs[indx] && attrs[indx].name;
                    if (removedAttr.test(attr)) {
                        // remove data-attributes - see #351
                        base.preview.removeAttribute(attr);
                    }
                }
                // build preview container and append preview display
                $('<div />')
                    .addClass(kbcss.wrapper)
                    .append(base.$preview)
                    .prependTo(base.$keyboard);
            } else {
                base.$preview = base.$el;
                base.preview = base.el;
            }
        };

        base.saveCaret = function (start, end, $el) {
            var p = $keyboard.caret($el || base.$preview, start, end);
            base.last.start = typeof start === 'undefined' ? p.start : start;
            base.last.end = typeof end === 'undefined' ? p.end : end;
        };

        base.setScroll = function () {
            // Set scroll so caret & current text is in view
            // needed for virtual keyboard typing, NOT manual typing - fixes #23
            if (base.last.virtual) {

                var scrollWidth, clientWidth, adjustment, direction,
                    isTextarea = base.preview.nodeName === 'TEXTAREA',
                    value = base.last.val.substring(0, Math.max(base.last.start, base.last.end));

                if (!base.$previewCopy) {
                    // clone preview
                    base.$previewCopy = base.$preview.clone()
                        .removeAttr('id') // fixes #334
                        .css({
                            position: 'absolute',
                            left: 0,
                            zIndex: -10,
                            visibility: 'hidden'
                        })
                        .addClass($keyboard.css.inputClone);
                    if (!isTextarea) {
                        // make input zero-width because we need an accurate scrollWidth
                        base.$previewCopy.css({
                            'white-space': 'pre',
                            'width': 0
                        });
                    }
                    if (o.usePreview) {
                        // add clone inside of preview wrapper
                        base.$preview.after(base.$previewCopy);
                    } else {
                        // just slap that thing in there somewhere
                        base.$keyboard.prepend(base.$previewCopy);
                    }
                }

                if (isTextarea) {
                    // need the textarea scrollHeight, so set the clone textarea height to be the line height
                    base.$previewCopy
                        .height(base.lineHeight)
                        .val(value);
                    // set scrollTop for Textarea
                    base.preview.scrollTop = base.lineHeight *
                        (Math.floor(base.$previewCopy[0].scrollHeight / base.lineHeight) - 1);
                } else {
                    // add non-breaking spaces
                    base.$previewCopy.val(value.replace(/\s/g, '\xa0'));

                    // if scrollAdjustment option is set to "c" or "center" then center the caret
                    adjustment = /c/i.test(o.scrollAdjustment) ? base.preview.clientWidth / 2 : o.scrollAdjustment;
                    scrollWidth = base.$previewCopy[0].scrollWidth - 1;

                    // set initial state as moving right
                    if (typeof base.last.scrollWidth === 'undefined') {
                        base.last.scrollWidth = scrollWidth;
                        base.last.direction = true;
                    }
                    // if direction = true; we're scrolling to the right
                    direction = base.last.scrollWidth === scrollWidth ?
                        base.last.direction :
                    base.last.scrollWidth < scrollWidth;
                    clientWidth = base.preview.clientWidth - adjustment;

                    // set scrollLeft for inputs; try to mimic the inherit caret positioning + scrolling:
                    // hug right while scrolling right...
                    if (direction) {
                        if (scrollWidth < clientWidth) {
                            base.preview.scrollLeft = 0;
                        } else {
                            base.preview.scrollLeft = scrollWidth - clientWidth;
                        }
                    } else {
                        // hug left while scrolling left...
                        if (scrollWidth >= base.preview.scrollWidth - clientWidth) {
                            base.preview.scrollLeft = base.preview.scrollWidth - adjustment;
                        } else if (scrollWidth - adjustment > 0) {
                            base.preview.scrollLeft = scrollWidth - adjustment;
                        } else {
                            base.preview.scrollLeft = 0;
                        }
                    }

                    base.last.scrollWidth = scrollWidth;
                    base.last.direction = direction;
                }
            }
        };

        base.bindFocus = function () {
            if (o.openOn) {
                // make sure keyboard isn't destroyed
                // Check if base exists, this is a case when destroy is called, before timers have fired
                if (base && base.el.active) {
                    base.$el.bind(o.openOn + base.namespace, function () {
                        base.focusOn();
                    });
                    // remove focus from element (needed for IE since blur doesn't seem to work)
                    if ($(':focus')[0] === base.el) {
                        base.$el.blur();
                    }
                }
            }
        };

        base.bindKeyboard = function () {
            var evt,
                keyCodes = $keyboard.keyCodes,
                layout = $keyboard.builtLayouts[base.layout];
            base.$preview
                .unbind(base.namespace)
                .bind('click' + base.namespace + ' touchstart' + base.namespace, function () {
                    if (o.alwaysOpen && !base.isCurrent()) {
                        base.reveal();
                    }
                    // update last caret position after user click, use at least 150ms or it doesn't work in IE
                    base.timer2 = setTimeout(function () {
                        if (base){
                            base.saveCaret();
                        }
                    }, 150);

                })
                .bind('keypress' + base.namespace, function (e) {
                    if (o.lockInput || !base.isCurrent()) {
                        return false;
                    }
                    var k = e.charCode || e.which,
                    // capsLock can only be checked while typing a-z
                        k1 = k >= keyCodes.A && k <= keyCodes.Z,
                        k2 = k >= keyCodes.a && k <= keyCodes.z,
                        str = base.last.key = String.fromCharCode(k);
                    base.last.virtual = false;
                    base.last.event = e;
                    base.last.$key = []; // not a virtual keyboard key
                    if (base.checkCaret) {
                        base.saveCaret();
                    }

                    // update capsLock
                    if (k !== keyCodes.capsLock && (k1 || k2)) {
                        base.capsLock = (k1 && !e.shiftKey) || (k2 && e.shiftKey);
                        // if shifted keyset not visible, then show it
                        if (base.capsLock && !base.shiftActive) {
                            base.shiftActive = true;
                            base.showSet();
                        }
                    }

                    // restrict input - keyCode in keypress special keys:
                    // see http://www.asquare.net/javascript/tests/KeyCode.html
                    if (o.restrictInput) {
                        // allow navigation keys to work - Chrome doesn't fire a keypress event (8 = bksp)
                        if ((e.which === keyCodes.backSpace || e.which === 0) &&
                            $.inArray(e.keyCode, base.alwaysAllowed)) {
                            return;
                        }
                        // quick key check
                        if ($.inArray(str, layout.acceptedKeys) === -1) {
                            e.preventDefault();
                            // copy event object in case e.preventDefault() breaks when changing the type
                            evt = $.extend({}, e);
                            evt.type = $keyboard.events.inputRestricted;
                            base.$el.trigger(evt, [base, base.el]);
                            if ($.isFunction(o.restricted)) {
                                o.restricted(evt, base, base.el);
                            }
                        }
                    } else if ((e.ctrlKey || e.metaKey) &&
                        (e.which === keyCodes.A || e.which === keyCodes.C || e.which === keyCodes.V ||
                        (e.which >= keyCodes.X && e.which <= keyCodes.Z))) {
                        // Allow select all (ctrl-a), copy (ctrl-c), paste (ctrl-v) & cut (ctrl-x) &
                        // redo (ctrl-y)& undo (ctrl-z); meta key for mac
                        return;
                    }
                    // Mapped Keys - allows typing on a regular keyboard and the mapped key is entered
                    // Set up a key in the layout as follows: 'm(a):label'; m = key to map, (a) = actual keyboard key
                    // to map to (optional), ':label' = title/tooltip (optional)
                    // example: \u0391 or \u0391(A) or \u0391:alpha or \u0391(A):alpha
                    if (layout.hasMappedKeys && layout.mappedKeys.hasOwnProperty(str)) {
                        base.last.key = layout.mappedKeys[str];
                        base.insertText(base.last.key);
                        e.preventDefault();
                    }
                    base.checkMaxLength();

                })
                .bind('keyup' + base.namespace, function (e) {
                    if (!base.isCurrent()) { return; }
                    base.last.virtual = false;
                    switch (e.which) {
                        // Insert tab key
                        case keyCodes.tab:
                            // Added a flag to prevent from tabbing into an input, keyboard opening, then adding the tab
                            // to the keyboard preview area on keyup. Sadly it still happens if you don't release the tab
                            // key immediately because keydown event auto-repeats
                            if (base.tab && o.tabNavigation && !o.lockInput) {
                                base.shiftActive = e.shiftKey;
                                // when switching inputs, the tab keyaction returns false
                                var notSwitching = $keyboard.keyaction.tab(base);
                                base.tab = false;
                                if (!notSwitching) {
                                    return false;
                                }
                            } else {
                                e.preventDefault();
                            }
                            break;

                        // Escape will hide the keyboard
                        case keyCodes.escape:
                            if (!o.ignoreEsc) {
                                base.close(o.autoAccept && o.autoAcceptOnEsc ? 'true' : false);
                            }
                            return false;
                    }

                    // throttle the check combo function because fast typers will have an incorrectly positioned caret
                    clearTimeout(base.throttled);
                    base.throttled = setTimeout(function () {
                        // fix error in OSX? see issue #102
                        if (base && base.isVisible()) {
                            base.checkCombos();
                        }
                    }, 100);

                    base.checkMaxLength();

                    base.last.preVal = '' + base.last.val;
                    base.last.val = base.$preview.val();
                    e.type = $keyboard.events.kbChange;
                    // base.last.key may be empty string (shift, enter, tab, etc) when keyboard is first visible
                    // use e.key instead, if browser supports it
                    e.action = base.last.key;
                    base.$el.trigger(e, [base, base.el]);

                    // change callback is no longer bound to the input element as the callback could be
                    // called during an external change event with all the necessary parameters (issue #157)
                    if ($.isFunction(o.change)) {
                        e.type = $keyboard.events.inputChange;
                        o.change(e, base, base.el);
                        return false;
                    }
                    if (o.acceptValid && o.autoAcceptOnValid) {
                        if ($.isFunction(o.validate) && o.validate(base, base.$preview.val())) {
                            base.$preview.blur();
                            base.accept();
                        }
                    }
                })
                .bind('keydown' + base.namespace, function (e) {
                    // ensure alwaysOpen keyboards are made active
                    if (o.alwaysOpen && !base.isCurrent()) {
                        base.reveal();
                    }
                    // prevent tab key from leaving the preview window
                    if (e.which === keyCodes.tab) {
                        // allow tab to pass through - tab to next input/shift-tab for prev
                        base.tab = true;
                        return false;
                    }

                    if (o.lockInput) {
                        return false;
                    }

                    base.last.virtual = false;
                    switch (e.which) {

                        case keyCodes.backSpace:
                            $keyboard.keyaction.bksp(base, null, e);
                            e.preventDefault();
                            break;

                        case keyCodes.enter:
                            $keyboard.keyaction.enter(base, null, e);
                            break;

                        // Show capsLock
                        case keyCodes.capsLock:
                            base.shiftActive = base.capsLock = !base.capsLock;
                            base.showSet();
                            break;

                        case keyCodes.V:
                            // prevent ctrl-v/cmd-v
                            if (e.ctrlKey || e.metaKey) {
                                if (o.preventPaste) {
                                    e.preventDefault();
                                    return;
                                }
                                base.checkCombos(); // check pasted content
                            }
                            break;
                    }
                })
                .bind('mouseup touchend '.split(' ').join(base.namespace + ' '), function () {
                    base.last.virtual = true;
                    base.saveCaret();
                });

            // prevent keyboard event bubbling
            base.$keyboard.bind('mousedown click touchstart '.split(' ').join(base.namespace + ' '), function (e) {
                e.stopPropagation();
                if (!base.isCurrent()) {
                    base.reveal();
                    $(document).trigger('checkkeyboard' + base.namespace);
                }
                if (!o.noFocus) {
                    base.$preview.focus();
                }
            });

            // If preventing paste, block context menu (right click)
            if (o.preventPaste) {
                base.$preview.bind('contextmenu' + base.namespace, function (e) {
                    e.preventDefault();
                });
                base.$el.bind('contextmenu' + base.namespace, function (e) {
                    e.preventDefault();
                });
            }

        };

        base.bindKeys = function () {
            var kbcss = $keyboard.css;
            base.$allKeys = base.$keyboard.find('button.' + kbcss.keyButton)
                .unbind(base.namespace + ' ' + base.namespace + 'kb')
                // Change hover class and tooltip - moved this touchstart before option.keyBinding touchstart
                // to prevent mousewheel lag/duplication - Fixes #379 & #411
                .bind('mouseenter mouseleave touchstart '.split(' ').join(base.namespace + ' '), function (e) {
                    if ((o.alwaysOpen || o.userClosed) && e.type !== 'mouseleave' && !base.isCurrent()) {
                        base.reveal();
                        base.$preview.focus();
                        $keyboard.caret(base.$preview, base.last);
                    }
                    if (!base.isCurrent()) {
                        return;
                    }
                    var $keys, txt,
                        last = base.last,
                        $this = $(this),
                        type = e.type;

                    if (o.useWheel && base.wheel) {
                        $keys = base.getLayers($this);
                        txt = ($keys.length ? $keys.map(function () {
                                    return $(this).attr('data-value') || '';
                                })
                                .get() : '') || [$this.text()];
                        last.wheel_$Keys = $keys;
                        last.wheelLayers = txt;
                        last.wheelIndex = $.inArray($this.attr('data-value'), txt);
                    }

                    if ((type === 'mouseenter' || type === 'touchstart') && base.el.type !== 'password' &&
                        !$this.hasClass(o.css.buttonDisabled)) {
                        $this.addClass(o.css.buttonHover);
                        if (o.useWheel && base.wheel) {
                            $this.attr('title', function (i, t) {
                                // show mouse wheel message
                                return (base.wheel && t === '' && base.sets && txt.length > 1 && type !== 'touchstart') ?
                                    o.wheelMessage : t;
                            });
                        }
                    }
                    if (type === 'mouseleave') {
                        // needed or IE flickers really bad
                        $this.removeClass((base.el.type === 'password') ? '' : o.css.buttonHover);
                        if (o.useWheel && base.wheel) {
                            last.wheelIndex = 0;
                            last.wheelLayers = [];
                            last.wheel_$Keys = null;
                            $this
                                .attr('title', function (i, t) {
                                    return (t === o.wheelMessage) ? '' : t;
                                })
                                .html($this.attr('data-html')); // restore original button text
                        }
                    }
                })
                // keyBinding = 'mousedown touchstart' by default
                .bind(o.keyBinding.split(' ').join(base.namespace + ' ') + base.namespace + ' ' +
                    $keyboard.events.kbRepeater, function (e) {
                    e.preventDefault();
                    // prevent errors when external triggers attempt to 'type' - see issue #158
                    if (!base.$keyboard.is(':visible')) {
                        return false;
                    }
                    var action, $keys,
                        last = base.last,
                        key = this,
                        $key = $(key),
                    // prevent mousedown & touchstart from both firing events at the same time - see #184
                        timer = new Date().getTime();

                    if (o.useWheel && base.wheel) {
                        // get keys from other layers/keysets (shift, alt, meta, etc) that line up by data-position
                        $keys = last.wheel_$Keys;
                        // target mousewheel selected key
                        $key = $keys && last.wheelIndex > -1 ? $keys.eq(last.wheelIndex) : $key;
                    }
                    action = $key.attr('data-action');
                    if (timer - (last.eventTime || 0) < o.preventDoubleEventTime) {
                        return;
                    }
                    last.eventTime = timer;
                    last.event = e;
                    last.virtual = true;
                    if (!o.noFocus) {
                        base.$preview.focus();
                    }
                    last.$key = $key;
                    last.key = $key.attr('data-value');
                    // Start caret in IE when not focused (happens with each virtual keyboard button click
                    if (base.checkCaret) {
                        $keyboard.caret(base.$preview, last);
                    }
                    if (action.match('meta')) {
                        action = 'meta';
                    }
                    // keyaction is added as a string, override original action & text
                    if (action === last.key && typeof $keyboard.keyaction[action] === 'string') {
                        last.key = action = $keyboard.keyaction[action];
                    } else if (action in $keyboard.keyaction && $.isFunction($keyboard.keyaction[action])) {
                        // stop processing if action returns false (close & cancel)
                        if ($keyboard.keyaction[action](base, this, e) === false) {
                            return false;
                        }
                        action = null; // prevent inserting action name
                    }
                    if (typeof action !== 'undefined' && action !== null) {
                        last.key = $(this).hasClass(kbcss.keyAction) ? action : last.key;
                        base.insertText(last.key);
                        if (!base.capsLock && !o.stickyShift && !e.shiftKey) {
                            base.shiftActive = false;
                            base.showSet($key.attr('data-name'));
                        }
                    }
                    // set caret if caret moved by action function; also, attempt to fix issue #131
                    $keyboard.caret(base.$preview, last);
                    base.checkCombos();
                    e.type = $keyboard.events.kbChange;
                    e.action = last.key;
                    base.$el.trigger(e, [base, base.el]);
                    last.preVal = '' + last.val;
                    last.val = base.$preview.val();

                    if ($.isFunction(o.change)) {
                        e.type = $keyboard.events.inputChange;
                        o.change(e, base, base.el);
                        // return false to prevent reopening keyboard if base.accept() was called
                        return false;
                    }

                })
                // using 'kb' namespace for mouse repeat functionality to keep it separate
                // I need to trigger a 'repeater.keyboard' to make it work
                .bind('mouseup' + base.namespace + ' ' + 'mouseleave touchend touchmove touchcancel '.split(' ')
                        .join(base.namespace + 'kb '), function (e) {
                    base.last.virtual = true;
                    var offset,
                        $this = $(this);
                    if (e.type === 'touchmove') {
                        // if moving within the same key, don't stop repeating
                        offset = $this.offset();
                        offset.right = offset.left + $this.outerWidth();
                        offset.bottom = offset.top + $this.outerHeight();
                        if (e.originalEvent.touches[0].pageX >= offset.left &&
                            e.originalEvent.touches[0].pageX < offset.right &&
                            e.originalEvent.touches[0].pageY >= offset.top &&
                            e.originalEvent.touches[0].pageY < offset.bottom) {
                            return true;
                        }
                    } else if (/(mouseleave|touchend|touchcancel)/i.test(e.type)) {
                        $this.removeClass(o.css.buttonHover); // needed for touch devices
                    } else {
                        if (!o.noFocus && base.isCurrent() && base.isVisible()) {
                            base.$preview.focus();
                        }
                        if (base.checkCaret) {
                            $keyboard.caret(base.$preview, base.last);
                        }
                    }
                    base.mouseRepeat = [false, ''];
                    clearTimeout(base.repeater); // make sure key repeat stops!
                    return false;
                })
                // prevent form submits when keyboard is bound locally - issue #64
                .bind('click' + base.namespace, function () {
                    return false;
                })
                // no mouse repeat for action keys (shift, ctrl, alt, meta, etc)
                .not('.' + kbcss.keyAction)
                // Allow mousewheel to scroll through other keysets of the same (non-action) key
                .bind('mousewheel' + base.namespace, function (e, delta) {
                    if (o.useWheel && base.wheel) {
                        // deltaY used by newer versions of mousewheel plugin
                        delta = delta || e.deltaY;
                        var n,
                            txt = base.last.wheelLayers || [];
                        if (txt.length > 1) {
                            n = base.last.wheelIndex + (delta > 0 ? -1 : 1);
                            if (n > txt.length - 1) {
                                n = 0;
                            }
                            if (n < 0) {
                                n = txt.length - 1;
                            }
                        } else {
                            n = 0;
                        }
                        base.last.wheelIndex = n;
                        $(this).html(txt[n]);
                        return false;
                    }
                })
                // mouse repeated action key exceptions
                .add('.' + kbcss.keyPrefix + ('tab bksp space enter'.split(' ')
                        .join(',.' + kbcss.keyPrefix)), base.$keyboard)
                .bind('mousedown touchstart '.split(' ').join(base.namespace + 'kb '), function () {
                    if (o.repeatRate !== 0) {
                        var key = $(this);
                        // save the key, make sure we are repeating the right one (fast typers)
                        base.mouseRepeat = [true, key];
                        setTimeout(function () {
                            // don't repeat keys if it is disabled - see #431
                            if (base && base.mouseRepeat[0] && base.mouseRepeat[1] === key && !key[0].disabled) {
                                base.repeatKey(key);
                            }
                        }, o.repeatDelay);
                    }
                    return false;
                });
        };

        // Insert text at caret/selection - thanks to Derek Wickwire for fixing this up!
        base.insertText = function (txt) {
            if (typeof txt === 'undefined') {
                return;
            }
            var bksp, t,
                isBksp = txt === '\b',
            // use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE).
                val = base.$preview.val(),
                pos = $keyboard.caret(base.$preview),
                len = val.length; // save original content length

            // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea
            // is still difficult
            // in IE, pos.end can be zero after input loses focus
            if (pos.end < pos.start) {
                pos.end = pos.start;
            }
            if (pos.start > len) {
                pos.end = pos.start = len;
            }

            if (base.preview.nodeName === 'TEXTAREA') {
                // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine)
                if ($keyboard.msie && val.substr(pos.start, 1) === '\n') {
                    pos.start += 1;
                    pos.end += 1;
                }
            }

            if (txt === '{d}') {
                txt = '';
                t = pos.start;
                pos.end += 1;
            }

            bksp = isBksp && pos.start === pos.end;
            txt = isBksp ? '' : txt;
            val = val.substr(0, pos.start - (bksp ? 1 : 0)) + txt + val.substr(pos.end);
            t = pos.start + (bksp ? -1 : txt.length);

            base.$preview.val(val);
            base.saveCaret(t, t); // save caret in case of bksp
            base.setScroll();
        };

        // check max length
        base.checkMaxLength = function () {
            if (!base.isCurrent()) { return; }
            var start, caret,
                val = base.$preview.val();
            if (o.maxLength !== false && val.length > o.maxLength) {
                start = $keyboard.caret(base.$preview).start;
                caret = Math.min(start, o.maxLength);

                // prevent inserting new characters when maxed #289
                if (!o.maxInsert) {
                    val = base.last.val;
                    caret = start - 1; // move caret back one
                }

                base.$preview.val(val.substring(0, o.maxLength));
                // restore caret on change, otherwise it ends up at the end.
                base.saveCaret(caret, caret);
            }
            if (base.$decBtn.length) {
                base.checkDecimal();
            }
        };

        // mousedown repeater
        base.repeatKey = function (key) {
            key.trigger($keyboard.events.kbRepeater);
            if (base.mouseRepeat[0]) {
                base.repeater = setTimeout(function () {
                    if (base){
                        base.repeatKey(key);
                    }
                }, base.repeatTime);
            }
        };

        // make it easier to switch keysets via API
        // showKeySet('shift+alt+meta1')
        base.showKeySet = function (str) {
            if (typeof str === 'string') {
                base.last.keyset = [base.shiftActive, base.altActive, base.metaActive];
                base.shiftActive = /shift/i.test(str);
                base.altActive = /alt/i.test(str);
                if (/meta/.test(str)) {
                    base.metaActive = true;
                    base.showSet(str.match(/meta\d+/i)[0]);
                } else {
                    base.metaActive = false;
                    base.showSet();
                }
            } else {
                base.showSet(str);
            }
        };

        base.showSet = function (name) {
            o = base.options; // refresh options
            var kbcss = $keyboard.css,
                prefix = '.' + kbcss.keyPrefix,
                active = o.css.buttonActive,
                key = '',
                toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0);
            if (!base.shiftActive) {
                base.capsLock = false;
            }
            // check meta key set
            if (base.metaActive) {
                // the name attribute contains the meta set # 'meta99'
                key = (/meta/i.test(name)) ? name : '';
                // save active meta keyset name
                if (key === '') {
                    key = (base.metaActive === true) ? '' : base.metaActive;
                } else {
                    base.metaActive = key;
                }
                // if meta keyset doesn't have a shift or alt keyset, then show just the meta key set
                if ((!o.stickyShift && base.last.keyset[2] !== base.metaActive) ||
                    ((base.shiftActive || base.altActive) &&
                    !base.$keyboard.find('.' + kbcss.keySet + '-' + key + base.rows[toShow]).length)) {
                    base.shiftActive = base.altActive = false;
                }
            } else if (!o.stickyShift && base.last.keyset[2] !== base.metaActive && base.shiftActive) {
                // switching from meta key set back to default, reset shift & alt if using stickyShift
                base.shiftActive = base.altActive = false;
            }
            toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0);
            key = (toShow === 0 && !base.metaActive) ? '-normal' : (key === '') ? '' : '-' + key;
            if (!base.$keyboard.find('.' + kbcss.keySet + key + base.rows[toShow]).length) {
                // keyset doesn't exist, so restore last keyset settings
                base.shiftActive = base.last.keyset[0];
                base.altActive = base.last.keyset[1];
                base.metaActive = base.last.keyset[2];
                return;
            }
            base.$keyboard
                .find(prefix + 'alt,' + prefix + 'shift,.' + kbcss.keyAction + '[class*=meta]')
                .removeClass(active)
                .end()
                .find(prefix + 'alt')
                .toggleClass(active, base.altActive)
                .end()
                .find(prefix + 'shift')
                .toggleClass(active, base.shiftActive)
                .end()
                .find(prefix + 'lock')
                .toggleClass(active, base.capsLock)
                .end()
                .find('.' + kbcss.keySet)
                .hide()
                .end()
                .find('.' + kbcss.keyAction + prefix + key)
                .addClass(active);

            // show keyset using inline-block ( extender layout will then line up )
            base.$keyboard.find('.' + kbcss.keySet + key + base.rows[toShow])[0].style.display = 'inline-block';

            if (base.metaActive) {
                base.$keyboard.find(prefix + base.metaActive)
                    // base.metaActive contains the string "meta#" or false
                    // without the !== false, jQuery UI tries to transition the classes
                    .toggleClass(active, base.metaActive !== false);
            }
            base.last.keyset = [base.shiftActive, base.altActive, base.metaActive];
            base.$el.trigger($keyboard.events.kbKeysetChange, [base, base.el]);
        };

        // check for key combos (dead keys)
        base.checkCombos = function () {
            // return val for close function
            if (!(base.isVisible() || base.$keyboard.hasClass($keyboard.css.hasFocus))) {
                return base.$preview.val();
            }
            var r, t, t2,
            // use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE).
                val = base.$preview.val(),
                pos = $keyboard.caret(base.$preview),
                layout = $keyboard.builtLayouts[base.layout],
                len = val.length; // save original content length
            // return if val is empty; fixes #352
            if (val === '') {
                // check valid on empty string - see #429
                if (o.acceptValid) {
                    base.checkValid();
                }
                return val;
            }

            // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea
            // is still difficult
            // in IE, pos.end can be zero after input loses focus
            if (pos.end < pos.start) {
                pos.end = pos.start;
            }
            if (pos.start > len) {
                pos.end = pos.start = len;
            }
            // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine)
            if ($keyboard.msie && val.substr(pos.start, 1) === '\n') {
                pos.start += 1;
                pos.end += 1;
            }

            if (o.useCombos) {
                // keep 'a' and 'o' in the regex for ae and oe ligature (æ,œ)
                // thanks to KennyTM: http://stackoverflow.com/q/4275077
                // original regex /([`\'~\^\"ao])([a-z])/mig moved to $.keyboard.comboRegex
                if ($keyboard.msie) {
                    // old IE may not have the caret positioned correctly, so just check the whole thing
                    val = val.replace(base.regex, function (s, accent, letter) {
                        return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s;
                    });
                    // prevent combo replace error, in case the keyboard closes - see issue #116
                } else if (base.$preview.length) {
                    // Modern browsers - check for combos from last two characters left of the caret
                    t = pos.start - (pos.start - 2 >= 0 ? 2 : 0);
                    // target last two characters
                    $keyboard.caret(base.$preview, t, pos.end);
                    // do combo replace
                    t2 = ($keyboard.caret(base.$preview).text || '').replace(base.regex, function (s, accent, letter) {
                        return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s;
                    });
                    // add combo back
                    base.$preview.val($keyboard.caret(base.$preview).replaceStr(t2));
                    val = base.$preview.val();
                }
            }

            // check input restrictions - in case content was pasted
            if (o.restrictInput && val !== '') {
                t = layout.acceptedKeys.length;

                r = layout.acceptedKeysRegex;
                if (!r) {
                    t2 = $.map(layout.acceptedKeys, function (v) {
                        // escape any special characters
                        return v.replace(base.escapeRegex, '\\$&');
                    });
                    r = layout.acceptedKeysRegex = new RegExp('(' + t2.join('|') + ')', 'g');
                }

                // only save matching keys
                t2 = val.match(r);
                if (t2) {
                    val = t2.join('');
                } else {
                    // no valid characters
                    val = '';
                    len = 0;
                }
            }

            // save changes, then reposition caret
            pos.start += val.length - len;
            pos.end += val.length - len;
            base.$preview.val(val);
            base.saveCaret(pos.start, pos.end);
            // set scroll to keep caret in view
            base.setScroll();

            base.checkMaxLength();

            if (o.acceptValid) {
                base.checkValid();
            }

            return val; // return text, used for keyboard closing section
        };

        // Toggle accept button classes, if validating
        base.checkValid = function () {
            var kbcss = $keyboard.css,
                $accept = base.$keyboard.find('.' + kbcss.keyPrefix + 'accept'),
                valid = true;
            if ($.isFunction(o.validate)) {
                valid = o.validate(base, base.$preview.val(), false);
            }
            // toggle accept button classes; defined in the css
            $accept
                .toggleClass(kbcss.inputInvalid, !valid)
                .toggleClass(kbcss.inputValid, valid)
                // update title to indicate that the entry is valid or invalid
                .attr('title', $accept.attr('data-title') + ' (' + o.display[valid ? 'valid' : 'invalid'] + ')');
        };

        // Decimal button for num pad - only allow one (not used by default)
        base.checkDecimal = function () {
            // Check US '.' or European ',' format
            if ((base.decimal && /\./g.test(base.preview.value)) ||
                (!base.decimal && /\,/g.test(base.preview.value))) {
                base.$decBtn
                    .attr({
                        'disabled': 'disabled',
                        'aria-disabled': 'true'
                    })
                    .removeClass(o.css.buttonHover)
                    .addClass(o.css.buttonDisabled);
            } else {
                base.$decBtn
                    .removeAttr('disabled')
                    .attr({
                        'aria-disabled': 'false'
                    })
                    .addClass(o.css.buttonDefault)
                    .removeClass(o.css.buttonDisabled);
            }
        };

        // get other layer values for a specific key
        base.getLayers = function ($el) {
            var kbcss = $keyboard.css,
                key = $el.attr('data-pos'),
                $keys = $el.closest('.' + kbcss.keyboard)
                    .find('button[data-pos="' + key + '"]');
            return $keys.filter(function () {
                    return $(this)
                            .find('.' + kbcss.keyText)
                            .text() !== '';
                })
                .add($el);
        };

        // Go to next or prev inputs
        // goToNext = true, then go to next input; if false go to prev
        // isAccepted is from autoAccept option or true if user presses shift+enter
        base.switchInput = function (goToNext, isAccepted) {
            if ($.isFunction(o.switchInput)) {
                o.switchInput(base, goToNext, isAccepted);
            } else {
                // base.$keyboard may be an empty array - see #275 (apod42)
                if (base.$keyboard.length) {
                    base.$keyboard.hide();
                }
                var kb,
                    stopped = false,
                    all = $('button, input, textarea, a')
                        .filter(':visible')
                        .not(':disabled'),
                    indx = all.index(base.$el) + (goToNext ? 1 : -1);
                if (base.$keyboard.length) {
                    base.$keyboard.show();
                }
                if (indx > all.length - 1) {
                    stopped = o.stopAtEnd;
                    indx = 0; // go to first input
                }
                if (indx < 0) {
                    stopped = o.stopAtEnd;
                    indx = all.length - 1; // stop or go to last
                }
                if (!stopped) {
                    isAccepted = base.close(isAccepted);
                    if (!isAccepted) {
                        return;
                    }
                    kb = all.eq(indx).data('keyboard');
                    if (kb && kb.options.openOn.length) {
                        kb.focusOn();
                    } else {
                        all.eq(indx).focus();
                    }
                }
            }
            return false;
        };

        // Close the keyboard, if visible. Pass a status of true, if the content was accepted
        // (for the event trigger).
        base.close = function (accepted) {
            if (base.isOpen && base.$keyboard.length) {
                clearTimeout(base.throttled);
                var kbcss = $keyboard.css,
                    kbevents = $keyboard.events,
                    val = (accepted) ? base.checkCombos() : base.originalContent;
                // validate input if accepted
                if (accepted && $.isFunction(o.validate) && !o.validate(base, val, true)) {
                    val = base.originalContent;
                    accepted = false;
                    if (o.cancelClose) {
                        return;
                    }
                }
                base.isCurrent(false);
                base.isOpen = o.alwaysOpen || o.userClosed;
                // update value for always open keyboards
                base.$preview.val(val);
                base.$el
                    .removeClass(kbcss.isCurrent + ' ' + kbcss.inputAutoAccepted)
                    // add 'ui-keyboard-autoaccepted' to inputs - see issue #66
                    .addClass((accepted || false) ? accepted === true ? '' : kbcss.inputAutoAccepted : '')
                    .val(val)
                    // trigger default change event - see issue #146
                    .trigger(kbevents.inputChange);
                // don't trigger an empty event - see issue #463
                if (!o.alwaysOpen) {
                    // don't trigger beforeClose if keyboard is always open
                    base.$el.trigger(kbevents.kbBeforeClose, [base, base.el, (accepted || false)]);
                }
                base.$el
                    .trigger(((accepted || false) ? kbevents.inputAccepted : kbevents.inputCanceled), [base, base.el])
                    .trigger((o.alwaysOpen) ? kbevents.kbInactive : kbevents.kbHidden, [base, base.el])
                    .blur();

                // save caret after updating value (fixes userClosed issue with changing focus)
                $keyboard.caret(base.$preview, base.last);
                // base is undefined if keyboard was destroyed - fixes #358
                if (base) {
                    // add close event time
                    base.last.eventTime = new Date().getTime();
                    if (!(o.alwaysOpen || o.userClosed && accepted === 'true') && base.$keyboard.length) {
                        // free up memory
                        base.removeKeyboard();
                        // rebind input focus - delayed to fix IE issue #72
                        base.timer = setTimeout(function () {
                            if(base){
                                base.bindFocus();
                            }
                        }, 500);
                    }
                    if (!base.watermark && base.el.value === '' && base.inPlaceholder !== '') {
                        base.$el
                            .addClass(kbcss.placeholder)
                            .val(base.inPlaceholder);
                    }
                }
            }
            return !!accepted;
        };

        base.accept = function () {
            return base.close(true);
        };

        base.checkClose = function (e) {
            if (base.opening) {
                return;
            }
            base.escClose(e);
            var kbcss = $.keyboard.css,
                $target = $(e.target);
            // needed for IE to allow switching between keyboards smoothly
            if ($target.hasClass(kbcss.input)) {
                var kb = $target.data('keyboard');
                // only trigger on self
                if (kb === base && !kb.$el.hasClass(kbcss.isCurrent) && e.type === kb.options.openOn) {
                    kb.focusOn();
                }
            }
        };

        base.escClose = function (e) {
            if (e && e.type === 'keyup') {
                return (e.which === $keyboard.keyCodes.escape && !o.ignoreEsc) ?
                    base.close(o.autoAccept && o.autoAcceptOnEsc ? 'true' : false) :
                    '';
            }
            // keep keyboard open if alwaysOpen or stayOpen is true - fixes mutliple always open keyboards or
            // single stay open keyboard
            if (!base.isOpen) {
                return;
            }
            // ignore autoaccept if using escape - good idea?
            if (!base.isCurrent() && base.isOpen || base.isOpen && e.target !== base.el) {
                // don't close if stayOpen is set; but close if a different keyboard is being opened
                if ((o.stayOpen || o.userClosed) && !$(e.target).hasClass($keyboard.css.input)) {
                    return;
                }
                // stop propogation in IE - an input getting focus doesn't open a keyboard if one is already open
                if ($keyboard.allie) {
                    e.preventDefault();
                }
                // send 'true' instead of a true (boolean), the input won't get a 'ui-keyboard-autoaccepted'
                // class name - see issue #66
                base.close(o.autoAccept ? 'true' : false);
            }
        };

        // Build default button
        base.keyBtn = $('<button />')
            .attr({
                'role': 'button',
                'type': 'button',
                'aria-disabled': 'false',
                'tabindex': '-1'
            })
            .addClass($keyboard.css.keyButton);

        // convert key names into a class name
        base.processName = function (name) {
            var index, n,
                process = (name || '').replace(/[^a-z0-9-_]/gi, ''),
                len = process.length,
                newName = [];
            if (len > 1 && name === process) {
                // return name if basic text
                return name;
            }
            // return character code sequence
            len = name.length;
            if (len) {
                for (index = 0; index < len; index++) {
                    n = name[index];
                    // keep '-' and '_'... so for dash, we get two dashes in a row
                    newName.push(/[a-z0-9-_]/i.test(n) ?
                            (/[-_]/.test(n) && index !== 0 ? '' : n) :
                        (index === 0 ? '' : '-') + n.charCodeAt(0)
                    );
                }
                return newName.join('');
            } else {
                return name;
            }
        };

        base.processKeys = function (name) {
            var tmp,
                parts = name.split(':'),
                data = {
                    name: null,
                    map: '',
                    title: ''
                };
            /* map defined keys
             format 'key(A):Label_for_key_(ignore_parentheses_here)'
             'key' = key that is seen (can any character(s); but it might need to be escaped using '\'
             or entered as unicode '\u####'
             '(A)' = the actual key on the real keyboard to remap
             ':Label_for_key' ends up in the title/tooltip
             Examples:
             '\u0391(A):alpha', 'x(y):this_(might)_cause_problems
             or edge cases of ':(x)', 'x(:)', 'x(()' or 'x())'
             Enhancement (if I can get alt keys to work):
             A mapped key will include the mod key, e.g. 'x(alt-x)' or 'x(alt-shift-x)'
             */
            if (/\(.+\)/.test(parts[0]) || /^:\(.+\)/.test(name) || /\([(:)]\)/.test(name)) {
                // edge cases 'x(:)', 'x(()' or 'x())'
                if (/\([(:)]\)/.test(name)) {
                    tmp = parts[0].match(/([^(]+)\((.+)\)/);
                    if (tmp && tmp.length) {
                        data.name = tmp[1];
                        data.map = tmp[2];
                        data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
                    } else {
                        // edge cases 'x(:)', ':(x)' or ':(:)'
                        data.name = name.match(/([^(]+)/)[0];
                        if (data.name === ':') {
                            // ':(:):test' => parts = [ '', '(', ')', 'title' ] need to slice 1
                            parts = parts.slice(1);
                        }
                        if (tmp === null) {
                            // 'x(:):test' => parts = [ 'x(', ')', 'title' ] need to slice 2
                            data.map = ':';
                            parts = parts.slice(2);
                        }
                        data.title = parts.length ? parts.join(':') : '';
                    }
                } else {
                    // example: \u0391(A):alpha; extract 'A' from '(A)'
                    data.map = name.match(/\(([^()]+?)\)/)[1];
                    // remove '(A)', left with '\u0391:alpha'
                    name = name.replace(/\(([^()]+)\)/, '');
                    tmp = name.split(':');
                    // get '\u0391' from '\u0391:alpha'
                    if (tmp[0] === '') {
                        data.name = ':';
                        parts = parts.slice(1);
                    } else {
                        data.name = tmp[0];
                    }
                    data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
                }
            } else {
                // find key label
                // corner case of '::;' reduced to ':;', split as ['', ';']
                if (parts[0] === '') {
                    data.name = ':';
                    parts = parts.slice(1);
                } else {
                    data.name = parts[0];
                }
                data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
            }
            data.title = $.trim(data.title).replace(/_/g, ' ');
            return data;
        };

        // Add key function
        // keyName = the name of the function called in $.keyboard.keyaction when the button is clicked
        // name = name added to key, or cross-referenced in the display options
        // base.temp[0] = keyset to attach the new button
        // regKey = true when it is not an action key
        base.addKey = function (keyName, action, regKey) {
            var keyClass, tmp, keys,
                data = {},
                txt = base.processKeys(regKey ? keyName : action),
                kbcss = $keyboard.css;

            if (!regKey && o.display[txt.name]) {
                keys = base.processKeys(o.display[txt.name]);
                // action contained in "keyName" (e.g. keyName = "accept",
                // action = "a" (use checkmark instead of text))
                keys.action = base.processKeys(keyName).name;
            } else {
                // when regKey is true, keyName is the same as action
                keys = txt;
                keys.action = txt.name;
            }

            data.name = base.processName(txt.name);

            if (keys.map !== '') {
                $keyboard.builtLayouts[base.layout].mappedKeys[keys.map] = keys.name;
                $keyboard.builtLayouts[base.layout].acceptedKeys.push(keys.name);
            } else if (regKey) {
                $keyboard.builtLayouts[base.layout].acceptedKeys.push(keys.name);
            }

            if (regKey) {
                keyClass = data.name === '' ? '' : kbcss.keyPrefix + data.name;
            } else {
                // Action keys will have the 'ui-keyboard-actionkey' class
                keyClass = kbcss.keyAction + ' ' + kbcss.keyPrefix + keys.action;
            }
            // '\u2190'.length = 1 because the unicode is converted, so if more than one character,
            // add the wide class
            keyClass += (keys.name.length > 2 ? ' ' + kbcss.keyWide : '') + ' ' + o.css.buttonDefault;

            data.html = '<span class="' + kbcss.keyText + '">' +
                    // this prevents HTML from being added to the key
                keys.name.replace(/[\u00A0-\u9999]/gim, function (i) {
                    return '&#' + i.charCodeAt(0) + ';';
                }) +
                '</span>';

            data.$key = base.keyBtn
                .clone()
                .attr({
                    'data-value': regKey ? keys.name : keys.action, // value
                    'data-name': keys.action,
                    'data-pos': base.temp[1] + ',' + base.temp[2],
                    'data-action': keys.action,
                    'data-html': data.html
                })
                // add 'ui-keyboard-' + data.name for all keys
                //  (e.g. 'Bksp' will have 'ui-keyboard-bskp' class)
                // any non-alphanumeric characters will be replaced with
                //  their decimal unicode value
                //  (e.g. '~' is a regular key, class = 'ui-keyboard-126'
                //  (126 is the unicode decimal value - same as &#126;)
                //  See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
                .addClass(keyClass)
                .html(data.html)
                .appendTo(base.temp[0]);

            if (keys.map) {
                data.$key.attr('data-mapped', keys.map);
            }
            if (keys.title || txt.title) {
                data.$key.attr({
                    'data-title': txt.title || keys.title, // used to allow adding content to title
                    'title': txt.title || keys.title
                });
            }

            if (typeof o.buildKey === 'function') {
                data = o.buildKey(base, data);
                // copy html back to attributes
                tmp = data.$key.html();
                data.$key.attr('data-html', tmp);
            }
            return data.$key;
        };

        base.customHash = function (layout) {
            /*jshint bitwise:false */
            var i, array, hash, character, len,
                arrays = [],
                merged = [];
            // pass layout to allow for testing
            layout = typeof layout === 'undefined' ? o.customLayout : layout;
            // get all layout arrays
            for (array in layout) {
                if (layout.hasOwnProperty(array)) {
                    arrays.push(layout[array]);
                }
            }
            // flatten array
            merged = merged.concat.apply(merged, arrays).join(' ');
            // produce hash name - http://stackoverflow.com/a/7616484/145346
            hash = 0;
            len = merged.length;
            if (len === 0) {
                return hash;
            }
            for (i = 0; i < len; i++) {
                character = merged.charCodeAt(i);
                hash = ((hash << 5) - hash) + character;
                hash = hash & hash; // Convert to 32bit integer
            }
            return hash;
        };

        base.buildKeyboard = function (name, internal) {
            // o.display is empty when this is called from the scramble extension (when alwaysOpen:true)
            if ($.isEmptyObject(o.display)) {
                // set keyboard language
                base.updateLanguage();
            }
            var row, $row, currentSet,
                kbcss = $keyboard.css,
                sets = 0,
                layout = $keyboard.builtLayouts[name || base.layout || o.layout] = {
                    mappedKeys: {},
                    acceptedKeys: []
                },
                acceptedKeys = layout.acceptedKeys = o.restrictInclude ?
                ('' + o.restrictInclude).split(/\s+/) || [] :
                    [],
            // using $layout temporarily to hold keyboard popup classnames
                $layout = kbcss.keyboard + ' ' + o.css.popup + ' ' + o.css.container +
                    (o.alwaysOpen || o.userClosed ? ' ' + kbcss.alwaysOpen : ''),

                container = $('<div />')
                    .addClass($layout)
                    .attr({
                        'role': 'textbox'
                    })
                    .hide();
            // verify layout or setup custom keyboard
            if ((internal && o.layout === 'custom') || !$keyboard.layouts.hasOwnProperty(o.layout)) {
                o.layout = 'custom';
                $layout = $keyboard.layouts.custom = o.customLayout || {
                        'normal': ['{cancel}']
                    };
            } else {
                $layout = $keyboard.layouts[internal ? o.layout : name || base.layout || o.layout];
            }

            // Main keyboard building loop
            $.each($layout, function (set, keySet) {
                // skip layout name & lang settings
                if (set !== '' && !/^(name|lang|rtl)$/i.test(set)) {
                    // keep backwards compatibility for change from default to normal naming
                    if (set === 'default') {
                        set = 'normal';
                    }
                    sets++;
                    $row = $('<div />')
                        .attr('name', set) // added for typing extension
                        .addClass(kbcss.keySet + ' ' + kbcss.keySet + '-' + set)
                        .appendTo(container)
                        .toggle(set === 'normal');

                    for (row = 0; row < keySet.length; row++) {
                        // remove extra spaces before spliting (regex probably could be improved)
                        currentSet = $.trim(keySet[row]).replace(/\{(\.?)[\s+]?:[\s+]?(\.?)\}/g, '{$1:$2}');
                        base.buildRow($row, row, currentSet.split(/\s+/), acceptedKeys);
                        $row.find('.' + kbcss.keyButton + ',.' + kbcss.keySpacer)
                            .filter(':last')
                            .after('<br class="' + kbcss.endRow + '"/>');
                    }
                }
            });

            if (sets > 1) {
                base.sets = true;
            }
            layout.hasMappedKeys = !($.isEmptyObject(layout.mappedKeys));
            layout.$keyboard = container;
            return container;
        };

        base.buildRow = function ($row, row, keys, acceptedKeys) {
            var t, txt, key, isAction, action, margin,
                kbcss = $keyboard.css;
            for (key = 0; key < keys.length; key++) {
                // used by addKey function
                base.temp = [$row, row, key];
                isAction = false;

                // ignore empty keys
                if (keys[key].length === 0) {
                    continue;
                }

                // process here if it's an action key
                if (/^\{\S+\}$/.test(keys[key])) {
                    action = keys[key].match(/^\{(\S+)\}$/)[1];
                    // add active class if there are double exclamation points in the name
                    if (/\!\!/.test(action)) {
                        action = action.replace('!!', '');
                        isAction = true;
                    }

                    // add empty space
                    if (/^sp:((\d+)?([\.|,]\d+)?)(em|px)?$/i.test(action)) {
                        // not perfect globalization, but allows you to use {sp:1,1em}, {sp:1.2em} or {sp:15px}
                        margin = parseFloat(action
                                .replace(/,/, '.')
                                .match(/^sp:((\d+)?([\.|,]\d+)?)(em|px)?$/i)[1] || 0
                        );
                        $('<span class="' + kbcss.keyText + '"></span>')
                        // previously {sp:1} would add 1em margin to each side of a 0 width span
                        // now Firefox doesn't seem to render 0px dimensions, so now we set the
                        // 1em margin x 2 for the width
                            .width((action.match(/px/i) ? margin + 'px' : (margin * 2) + 'em'))
                            .addClass(kbcss.keySpacer)
                            .appendTo($row);
                    }

                    // add empty button
                    if (/^empty(:((\d+)?([\.|,]\d+)?)(em|px)?)?$/i.test(action)) {
                        margin = (/:/.test(action)) ? parseFloat(action
                                .replace(/,/, '.')
                                .match(/^empty:((\d+)?([\.|,]\d+)?)(em|px)?$/i)[1] || 0
                        ) : '';
                        base
                            .addKey('', ' ')
                            .addClass(o.css.buttonDisabled + ' ' + o.css.buttonEmpty)
                            .attr('aria-disabled', true)
                            .width(margin ? (action.match('px') ? margin + 'px' : (margin * 2) + 'em') : '');
                    }

                    // meta keys
                    if (/^meta\d+\:?(\w+)?/i.test(action)) {
                        base
                            .addKey(action.split(':')[0], action)
                            .addClass(kbcss.keyHasActive);
                        continue;
                    }

                    // switch needed for action keys with multiple names/shortcuts or
                    // default will catch all others
                    txt = action.split(':');
                    switch (txt[0].toLowerCase()) {

                        case 'a':
                        case 'accept':
                            base
                                .addKey('accept', action)
                                .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
                            break;

                        case 'alt':
                        case 'altgr':
                            base
                                .addKey('alt', action)
                                .addClass(kbcss.keyHasActive);
                            break;

                        case 'b':
                        case 'bksp':
                            base.addKey('bksp', action);
                            break;

                        case 'c':
                        case 'cancel':
                            base
                                .addKey('cancel', action)
                                .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
                            break;

                        // toggle combo/diacritic key
                        /*jshint -W083 */
                        case 'combo':
                            base
                                .addKey('combo', action)
                                .addClass(kbcss.keyHasActive)
                                .attr('title', function (indx, title) {
                                    // add combo key state to title
                                    return title + ' ' + o.display[o.useCombos ? 'active' : 'disabled'];
                                })
                                .toggleClass(o.css.buttonActive, o.useCombos);
                            break;

                        // Decimal - unique decimal point (num pad layout)
                        case 'dec':
                            acceptedKeys.push((base.decimal) ? '.' : ',');
                            base.addKey('dec', action);
                            break;

                        case 'e':
                        case 'enter':
                            base
                                .addKey('enter', action)
                                .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
                            break;

                        case 'lock':
                            base
                                .addKey('lock', action)
                                .addClass(kbcss.keyHasActive);
                            break;

                        case 's':
                        case 'shift':
                            base
                                .addKey('shift', action)
                                .addClass(kbcss.keyHasActive);
                            break;

                        // Change sign (for num pad layout)
                        case 'sign':
                            acceptedKeys.push('-');
                            base.addKey('sign', action);
                            break;

                        case 'space':
                            acceptedKeys.push(' ');
                            base.addKey('space', action);
                            break;

                        case 't':
                        case 'tab':
                            base.addKey('tab', action);
                            break;

                        default:
                            if ($keyboard.keyaction.hasOwnProperty(txt[0])) {
                                base
                                    .addKey(txt[0], action)
                                    .toggleClass(o.css.buttonAction + ' ' + kbcss.keyAction, isAction);
                            }

                    }

                } else {

                    // regular button (not an action key)
                    t = keys[key];
                    base.addKey(t, t, true);
                }
            }
        };

        base.removeBindings = function (namespace) {
            $(document).unbind(namespace);
            if (base.el.ownerDocument !== document) {
                $(base.el.ownerDocument).unbind(namespace);
            }
            $(window).unbind(namespace);
            base.$el.unbind(namespace);
        };

        base.removeKeyboard = function () {
            base.$allKeys = null;
            base.$decBtn = null;
            // base.$preview === base.$el when o.usePreview is false - fixes #442
            if (o.usePreview) {
                base.$preview.removeData('keyboard');
            }
            base.preview = null;
            base.$preview = null;
            base.$previewCopy = null;
            base.$keyboard.removeData('keyboard');
            base.$keyboard.remove();
            base.$keyboard = [];
            base.isOpen = false;
            base.isCurrent(false);
        };

        base.destroy = function (callback) {
            var index,
                kbcss = $keyboard.css,
                len = base.extensionNamespace.length,
                tmp = [
                    kbcss.input,
                    kbcss.locked,
                    kbcss.placeholder,
                    kbcss.noKeyboard,
                    kbcss.alwaysOpen,
                    o.css.input,
                    kbcss.isCurrent
                ].join(' ');
            clearTimeout(base.timer);
            clearTimeout(base.timer2);
            if (base.$keyboard.length) {
                base.removeKeyboard();
            }
            base.removeBindings(base.namespace);
            base.removeBindings(base.namespace + 'callbacks');
            for (index = 0; index < len; index++) {
                base.removeBindings(base.extensionNamespace[index]);
            }
            base.el.active = false;

            base.$el
                .removeClass(tmp)
                .removeAttr('aria-haspopup')
                .removeAttr('role')
                .removeData('keyboard');
            base = null;

            if (typeof callback === 'function') {
                callback();
            }
        };

        // Run initializer
        base.init();

    }; // end $.keyboard definition

    // event.which & ASCII values
    $keyboard.keyCodes = {
        backSpace: 8,
        tab: 9,
        enter: 13,
        capsLock: 20,
        escape: 27,
        space: 32,
        pageUp: 33,
        pageDown: 34,
        end: 35,
        home: 36,
        left: 37,
        up: 38,
        right: 39,
        down: 40,
        insert: 45,
        delete: 46,
        // event.which keyCodes (uppercase letters)
        A: 65,
        Z: 90,
        V: 86,
        C: 67,
        X: 88,

        // ASCII lowercase a & z
        a: 97,
        z: 122
    };

    $keyboard.css = {
        // keyboard id suffix
        idSuffix: '_keyboard',
        // element class names
        input: 'ui-keyboard-input',
        inputClone: 'ui-keyboard-preview-clone',
        wrapper: 'ui-keyboard-preview-wrapper',
        preview: 'ui-keyboard-preview',
        keyboard: 'ui-keyboard',
        keySet: 'ui-keyboard-keyset',
        keyButton: 'ui-keyboard-button',
        keyWide: 'ui-keyboard-widekey',
        keyPrefix: 'ui-keyboard-',
        keyText: 'ui-keyboard-text', // span with button text
        keyHasActive: 'ui-keyboard-hasactivestate',
        keyAction: 'ui-keyboard-actionkey',
        keySpacer: 'ui-keyboard-spacer', // empty keys
        keyToggle: 'ui-keyboard-toggle',
        keyDisabled: 'ui-keyboard-disabled',
        // states
        locked: 'ui-keyboard-lockedinput',
        alwaysOpen: 'ui-keyboard-always-open',
        noKeyboard: 'ui-keyboard-nokeyboard',
        placeholder: 'ui-keyboard-placeholder',
        hasFocus: 'ui-keyboard-has-focus',
        isCurrent: 'ui-keyboard-input-current',
        // validation & autoaccept
        inputValid: 'ui-keyboard-valid-input',
        inputInvalid: 'ui-keyboard-invalid-input',
        inputAutoAccepted: 'ui-keyboard-autoaccepted',
        endRow: 'ui-keyboard-button-endrow' // class added to <br>
    };

    $keyboard.events = {
        // keyboard events
        kbChange: 'keyboardChange',
        kbBeforeClose: 'beforeClose',
        kbBeforeVisible: 'beforeVisible',
        kbVisible: 'visible',
        kbInit: 'initialized',
        kbInactive: 'inactive',
        kbHidden: 'hidden',
        kbRepeater: 'repeater',
        kbKeysetChange: 'keysetChange',
        // input events
        inputAccepted: 'accepted',
        inputCanceled: 'canceled',
        inputChange: 'change',
        inputRestricted: 'restricted'
    };

    // Action key function list
    $keyboard.keyaction = {
        accept: function (base) {
            base.close(true); // same as base.accept();
            return false; // return false prevents further processing
        },
        alt: function (base) {
            base.altActive = !base.altActive;
            base.showSet();
        },
        bksp: function (base) {
            // the script looks for the '\b' string and initiates a backspace
            base.insertText('\b');
        },
        cancel: function (base) {
            base.close();
            return false; // return false prevents further processing
        },
        clear: function (base) {
            base.$preview.val('');
            if (base.$decBtn.length) {
                base.checkDecimal();
            }
        },
        combo: function (base) {
            var o = base.options,
                c = !o.useCombos,
                $combo = base.$keyboard.find('.' + $keyboard.css.keyPrefix + 'combo');
            o.useCombos = c;
            $combo
                .toggleClass(o.css.buttonActive, c)
                // update combo key state
                .attr('title', $combo.attr('data-title') + ' (' + o.display[c ? 'active' : 'disabled'] + ')');
            if (c) {
                base.checkCombos();
            }
            return false;
        },
        dec: function (base) {
            base.insertText((base.decimal) ? '.' : ',');
        },
        del: function (base) {
            // the script looks for the '{d}' string and initiates a delete
            base.insertText('{d}');
        },
        // resets to base keyset (deprecated because "default" is a reserved word)
        'default': function (base) {
            base.shiftActive = base.altActive = base.metaActive = false;
            base.showSet();
        },
        // el is the pressed key (button) object; it is null when the real keyboard enter is pressed
        enter: function (base, el, e) {
            var tag = base.el.nodeName,
                o = base.options;
            // shift+enter in textareas
            if (e.shiftKey) {
                // textarea & input - enterMod + shift + enter = accept, then go to prev;
                //  base.switchInput(goToNext, autoAccept)
                // textarea & input - shift + enter = accept (no navigation)
                return (o.enterNavigation) ? base.switchInput(!e[o.enterMod], true) : base.close(true);
            }
            // input only - enterMod + enter to navigate
            if (o.enterNavigation && (tag !== 'TEXTAREA' || e[o.enterMod])) {
                return base.switchInput(!e[o.enterMod], o.autoAccept ? 'true' : false);
            }
            // pressing virtual enter button inside of a textarea - add a carriage return
            // e.target is span when clicking on text and button at other times
            if (tag === 'TEXTAREA' && $(e.target).closest('button').length) {
                // IE8 fix (space + \n) - fixes #71 thanks Blookie!
                base.insertText(($keyboard.msie ? ' ' : '') + '\n');
            }
        },
        // caps lock key
        lock: function (base) {
            base.last.keyset[0] = base.shiftActive = base.capsLock = !base.capsLock;
            base.showSet();
        },
        left: function (base) {
            var p = $keyboard.caret(base.$preview);
            if (p.start - 1 >= 0) {
                // move both start and end of caret (prevents text selection) & save caret position
                base.last.start = base.last.end = p.start - 1;
                $keyboard.caret(base.$preview, base.last);
                base.setScroll();
            }
        },
        meta: function (base, el) {
            var $el = $(el);
            base.metaActive = !$el.hasClass(base.options.css.buttonActive);
            base.showSet($el.attr('data-name'));
        },
        next: function (base) {
            base.switchInput(true, base.options.autoAccept);
            return false;
        },
        // same as 'default' - resets to base keyset
        normal: function (base) {
            base.shiftActive = base.altActive = base.metaActive = false;
            base.showSet();
        },
        prev: function (base) {
            base.switchInput(false, base.options.autoAccept);
            return false;
        },
        right: function (base) {
            var p = $keyboard.caret(base.$preview);
            if (p.start + 1 <= base.$preview.val().length) {
                // move both start and end of caret (prevents text selection) && save caret position
                base.last.start = base.last.end = p.start + 1;
                $keyboard.caret(base.$preview, base.last);
                base.setScroll();
            }
        },
        shift: function (base) {
            base.last.keyset[0] = base.shiftActive = !base.shiftActive;
            base.showSet();
        },
        sign: function (base) {
            if (/^\-?\d*\.?\d*$/.test(base.$preview.val())) {
                base.$preview.val((base.$preview.val() * -1));
            }
        },
        space: function (base) {
            base.insertText(' ');
        },
        tab: function (base) {
            var tag = base.el.nodeName,
                o = base.options;
            if (tag === 'INPUT') {
                if (o.tabNavigation) {
                    return base.switchInput(!base.shiftActive, true);
                } else {
                    // ignore tab key in input
                    return false;
                }
            }
            base.insertText('\t');
        },
        toggle: function (base) {
            base.enabled = !base.enabled;
            base.toggle();
        },
        // *** Special action keys: NBSP & zero-width characters ***
        // Non-breaking space
        NBSP: '\u00a0',
        // zero width space
        ZWSP: '\u200b',
        // Zero width non-joiner
        ZWNJ: '\u200c',
        // Zero width joiner
        ZWJ: '\u200d',
        // Left-to-right Mark
        LRM: '\u200e',
        // Right-to-left Mark
        RLM: '\u200f'
    };

    // Default keyboard layouts
    $keyboard.builtLayouts = {};
    $keyboard.layouts = {
        'alpha': {
            'normal': [
                '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
                '{tab} a b c d e f g h i j [ ] \\',
                'k l m n o p q r s ; \' {enter}',
                '{shift} t u v w x y z , . / {shift}',
                '{accept} {space} {cancel}'
            ],
            'shift': [
                '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
                '{tab} A B C D E F G H I J { } |',
                'K L M N O P Q R S : " {enter}',
                '{shift} T U V W X Y Z < > ? {shift}',
                '{accept} {space} {cancel}'
            ]
        },
        'qwerty': {
            'normal': [
                '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
                '{tab} q w e r t y u i o p [ ] \\',
                'a s d f g h j k l ; \' {enter}',
                '{shift} z x c v b n m , . / {shift}',
                '{accept} {space} {cancel}'
            ],
            'shift': [
                '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
                '{tab} Q W E R T Y U I O P { } |',
                'A S D F G H J K L : " {enter}',
                '{shift} Z X C V B N M < > ? {shift}',
                '{accept} {space} {cancel}'
            ]
        },
        'international': {
            'normal': [
                '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
                '{tab} q w e r t y u i o p [ ] \\',
                'a s d f g h j k l ; \' {enter}',
                '{shift} z x c v b n m , . / {shift}',
                '{accept} {alt} {space} {alt} {cancel}'
            ],
            'shift': [
                '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
                '{tab} Q W E R T Y U I O P { } |',
                'A S D F G H J K L : " {enter}',
                '{shift} Z X C V B N M < > ? {shift}',
                '{accept} {alt} {space} {alt} {cancel}'
            ],
            'alt': [
                '~ \u00a1 \u00b2 \u00b3 \u00a4 \u20ac \u00bc \u00bd \u00be \u2018 \u2019 \u00a5 \u00d7 {bksp}',
                '{tab} \u00e4 \u00e5 \u00e9 \u00ae \u00fe \u00fc \u00fa \u00ed \u00f3 \u00f6 \u00ab \u00bb \u00ac',
                '\u00e1 \u00df \u00f0 f g h j k \u00f8 \u00b6 \u00b4 {enter}',
                '{shift} \u00e6 x \u00a9 v b \u00f1 \u00b5 \u00e7 > \u00bf {shift}',
                '{accept} {alt} {space} {alt} {cancel}'
            ],
            'alt-shift': [
                '~ \u00b9 \u00b2 \u00b3 \u00a3 \u20ac \u00bc \u00bd \u00be \u2018 \u2019 \u00a5 \u00f7 {bksp}',
                '{tab} \u00c4 \u00c5 \u00c9 \u00ae \u00de \u00dc \u00da \u00cd \u00d3 \u00d6 \u00ab \u00bb \u00a6',
                '\u00c4 \u00a7 \u00d0 F G H J K \u00d8 \u00b0 \u00a8 {enter}',
                '{shift} \u00c6 X \u00a2 V B \u00d1 \u00b5 \u00c7 . \u00bf {shift}',
                '{accept} {alt} {space} {alt} {cancel}'
            ]
        },
        'colemak': {
            'normal': [
                '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
                '{tab} q w f p g j l u y ; [ ] \\',
                '{bksp} a r s t d h n e i o \' {enter}',
                '{shift} z x c v b k m , . / {shift}',
                '{accept} {space} {cancel}'
            ],
            'shift': [
                '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
                '{tab} Q W F P G J L U Y : { } |',
                '{bksp} A R S T D H N E I O " {enter}',
                '{shift} Z X C V B K M < > ? {shift}',
                '{accept} {space} {cancel}'
            ]
        },
        'dvorak': {
            'normal': [
                '` 1 2 3 4 5 6 7 8 9 0 [ ] {bksp}',
                '{tab} \' , . p y f g c r l / = \\',
                'a o e u i d h t n s - {enter}',
                '{shift} ; q j k x b m w v z {shift}',
                '{accept} {space} {cancel}'
            ],
            'shift': [
                '~ ! @ # $ % ^ & * ( ) { } {bksp}',
                '{tab} " < > P Y F G C R L ? + |',
                'A O E U I D H T N S _ {enter}',
                '{shift} : Q J K X B M W V Z {shift}',
                '{accept} {space} {cancel}'
            ]
        },
        'num': {
            'normal': [
                '= ( ) {b}',
                '{clear} / * -',
                '7 8 9 +',
                '4 5 6 {sign}',
                '1 2 3 %',
                '0 {dec} {a} {c}'
            ]
        }
    };

    $keyboard.language = {
        en: {
            display: {
                // check mark - same action as accept
                'a': '\u2714:Accept (Shift+Enter)',
                'accept': 'Accept:Accept (Shift+Enter)',
                // other alternatives \u2311
                'alt': 'Alt:\u2325 AltGr',
                // Left arrow (same as &larr;)
                'b': '\u232b:Backspace',
                'bksp': 'Bksp:Backspace',
                // big X, close - same action as cancel
                'c': '\u2716:Cancel (Esc)',
                'cancel': 'Cancel:Cancel (Esc)',
                // clear num pad
                'clear': 'C:Clear',
                'combo': '\u00f6:Toggle Combo Keys',
                // decimal point for num pad (optional), change '.' to ',' for European format
                'dec': '.:Decimal',
                // down, then left arrow - enter symbol
                'e': '\u23ce:Enter',
                'empty': '\u00a0',
                'enter': 'Enter:Enter \u23ce',
                // left arrow (move caret)
                'left': '\u2190',
                // caps lock
                'lock': 'Lock:\u21ea Caps Lock',
                'next': 'Next \u21e8',
                'prev': '\u21e6 Prev',
                // right arrow (move caret)
                'right': '\u2192',
                // thick hollow up arrow
                's': '\u21e7:Shift',
                'shift': 'Shift:Shift',
                // +/- sign for num pad
                'sign': '\u00b1:Change Sign',
                'space': '\u00a0:Space',
                // right arrow to bar (used since this virtual keyboard works with one directional tabs)
                't': '\u21e5:Tab',
                // \u21b9 is the true tab symbol (left & right arrows)
                'tab': '\u21e5 Tab:Tab',
                // replaced by an image
                'toggle': ' ',

                // added to titles of keys
                // accept key status when acceptValid:true
                'valid': 'valid',
                'invalid': 'invalid',
                // combo key states
                'active': 'active',
                'disabled': 'disabled'
            },

            // Message added to the key title while hovering, if the mousewheel plugin exists
            wheelMessage: 'Use mousewheel to see other keys',

            comboRegex: /([`\'~\^\"ao])([a-z])/mig,
            combos: {
                // grave
                '`': { a: '\u00e0', A: '\u00c0', e: '\u00e8', E: '\u00c8', i: '\u00ec', I: '\u00cc', o: '\u00f2',
                    O: '\u00d2', u: '\u00f9', U: '\u00d9', y: '\u1ef3', Y: '\u1ef2' },
                // acute & cedilla
                "'": { a: '\u00e1', A: '\u00c1', e: '\u00e9', E: '\u00c9', i: '\u00ed', I: '\u00cd', o: '\u00f3',
                    O: '\u00d3', u: '\u00fa', U: '\u00da', y: '\u00fd', Y: '\u00dd' },
                // umlaut/trema
                '"': { a: '\u00e4', A: '\u00c4', e: '\u00eb', E: '\u00cb', i: '\u00ef', I: '\u00cf', o: '\u00f6',
                    O: '\u00d6', u: '\u00fc', U: '\u00dc', y: '\u00ff', Y: '\u0178' },
                // circumflex
                '^': { a: '\u00e2', A: '\u00c2', e: '\u00ea', E: '\u00ca', i: '\u00ee', I: '\u00ce', o: '\u00f4',
                    O: '\u00d4', u: '\u00fb', U: '\u00db', y: '\u0177', Y: '\u0176' },
                // tilde
                '~': { a: '\u00e3', A: '\u00c3', e: '\u1ebd', E: '\u1ebc', i: '\u0129', I: '\u0128', o: '\u00f5',
                    O: '\u00d5', u: '\u0169', U: '\u0168', y: '\u1ef9', Y: '\u1ef8', n: '\u00f1', N: '\u00d1' }
            }
        }
    };

    $keyboard.defaultOptions = {
        // set this to ISO 639-1 language code to override language set by the layout
        // http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
        // language defaults to 'en' if not found
        language: null,
        rtl: false,

        // *** choose layout & positioning ***
        layout: 'qwerty',
        customLayout: null,

        position: {
            // optional - null (attach to input/textarea) or a jQuery object (attach elsewhere)
            of: null,
            my: 'center top',
            at: 'center top',
            // used when 'usePreview' is false (centers the keyboard at the bottom of the input/textarea)
            at2: 'center bottom'
        },

        // allow jQuery position utility to reposition the keyboard on window resize
        reposition: true,

        // preview added above keyboard if true, original input/textarea used if false
        usePreview: true,

        // if true, the keyboard will always be visible
        alwaysOpen: false,

        // give the preview initial focus when the keyboard becomes visible
        initialFocus: true,

        // avoid changing the focus (hardware keyboard probably won't work)
        noFocus: false,

        // if true, keyboard will remain open even if the input loses focus, but closes on escape
        // or when another keyboard opens.
        stayOpen: false,

        // if true, keyboard will not close if you press escape.
        ignoreEsc: false,

        css: {
            // input & preview
            input: 'ui-widget-content ui-corner-all',
            // keyboard container
            container: 'ui-widget-content ui-widget ui-corner-all ui-helper-clearfix',
            // keyboard container extra class (same as container, but separate)
            popup: '',
            // default state
            buttonDefault: 'ui-state-default ui-corner-all',
            // hovered button
            buttonHover: 'ui-state-hover',
            // Action keys (e.g. Accept, Cancel, Tab, etc); this replaces 'actionClass' option
            buttonAction: 'ui-state-active',
            // Active keys (e.g. shift down, meta keyset active, combo keys active)
            buttonActive: 'ui-state-active',
            // used when disabling the decimal button {dec} when a decimal exists in the input area
            buttonDisabled: 'ui-state-disabled',
            buttonEmpty: 'ui-keyboard-empty'
        },

        // *** Useability ***
        // Auto-accept content when clicking outside the keyboard (popup will close)
        autoAccept: false,
        // Auto-accept content even if the user presses escape (only works if `autoAccept` is `true`)
        autoAcceptOnEsc: false,

        // Prevents direct input in the preview window when true
        lockInput: false,

        // Prevent keys not in the displayed keyboard from being typed in
        restrictInput: false,
        // Additional allowed characters while restrictInput is true
        restrictInclude: '', // e.g. 'a b foo \ud83d\ude38'

        // Check input against validate function, if valid the accept button gets a class name of
        // 'ui-keyboard-valid-input'. If invalid, the accept button gets a class name of
        // 'ui-keyboard-invalid-input'
        acceptValid: false,
        // Auto-accept when input is valid; requires `acceptValid` set `true` & validate callback
        autoAcceptOnValid: false,

        // if acceptValid is true & the validate function returns a false, this option will cancel
        // a keyboard close only after the accept button is pressed
        cancelClose: true,

        // tab to go to next, shift-tab for previous (default behavior)
        tabNavigation: false,

        // enter for next input; shift+enter accepts content & goes to next
        // shift + 'enterMod' + enter ('enterMod' is the alt as set below) will accept content and go
        // to previous in a textarea
        enterNavigation: false,
        // mod key options: 'ctrlKey', 'shiftKey', 'altKey', 'metaKey' (MAC only)
        enterMod: 'altKey', // alt-enter to go to previous; shift-alt-enter to accept & go to previous

        // if true, the next button will stop on the last keyboard input/textarea; prev button stops at first
        // if false, the next button will wrap to target the first input/textarea; prev will go to the last
        stopAtEnd: true,

        // Set this to append the keyboard after the input/textarea (appended to the input/textarea parent).
        // This option works best when the input container doesn't have a set width & when the 'tabNavigation'
        // option is true.
        appendLocally: false,
        // When appendLocally is false, the keyboard will be appended to this object
        appendTo: 'body',

        // If false, the shift key will remain active until the next key is (mouse) clicked on; if true it will
        // stay active until pressed again
        stickyShift: true,

        // Prevent pasting content into the area
        preventPaste: false,

        // caret placed at the end of any text when keyboard becomes visible
        caretToEnd: false,

        // caret stays this many pixels from the edge of the input while scrolling left/right;
        // use "c" or "center" to center the caret while scrolling
        scrollAdjustment: 10,

        // Set the max number of characters allowed in the input, setting it to false disables this option
        maxLength: false,
        // allow inserting characters @ caret when maxLength is set
        maxInsert: true,

        // Mouse repeat delay - when clicking/touching a virtual keyboard key, after this delay the key will
        // start repeating
        repeatDelay: 500,

        // Mouse repeat rate - after the repeatDelay, this is the rate (characters per second) at which the
        // key is repeated Added to simulate holding down a real keyboard key and having it repeat. I haven't
        // calculated the upper limit of this rate, but it is limited to how fast the javascript can process
        // the keys. And for me, in Firefox, it's around 20.
        repeatRate: 20,

        // resets the keyboard to the default keyset when visible
        resetDefault: true,

        // Event (namespaced) on the input to reveal the keyboard. To disable it, just set it to ''.
        openOn: 'focus',

        // Event (namepaced) for when the character is added to the input (clicking on the keyboard)
        keyBinding: 'mousedown touchstart',

        // enable/disable mousewheel functionality
        // enabling still depends on the mousewheel plugin
        useWheel: true,

        // combos (emulate dead keys : http://en.wikipedia.org/wiki/Keyboard_layout#US-International)
        // if user inputs `a the script converts it to à, ^o becomes ô, etc.
        useCombos: true,

        /*
         // *** Methods ***
         // commenting these out to reduce the size of the minified version
         // Callbacks - attach a function to any of these callbacks as desired
         initialized   : function(e, keyboard, el) {},
         beforeVisible : function(e, keyboard, el) {},
         visible       : function(e, keyboard, el) {},
         change        : function(e, keyboard, el) {},
         beforeClose   : function(e, keyboard, el, accepted) {},
         accepted      : function(e, keyboard, el) {},
         canceled      : function(e, keyboard, el) {},
         restricted    : function(e, keyboard, el) {},
         hidden        : function(e, keyboard, el) {},
         // called instead of base.switchInput
         switchInput   : function(keyboard, goToNext, isAccepted) {},
         // used if you want to create a custom layout or modify the built-in keyboard
         create        : function(keyboard) { return keyboard.buildKeyboard(); },

         // build key callback
         buildKey : function( keyboard, data ) {
         / *
         data = {
         // READ ONLY
         isAction : [boolean] true if key is an action key
         name     : [string]  key class name suffix ( prefix = 'ui-keyboard-' );
         may include decimal ascii value of character
         value    : [string]  text inserted (non-action keys)
         title    : [string]  title attribute of key
         action   : [string]  keyaction name
         html     : [string]  HTML of the key; it includes a <span> wrapping the text
         // use to modify key HTML
         $key     : [object]  jQuery selector of key which is already appended to keyboard
         }
         * /
         return data;
         },
         */

        // this callback is called, if the acceptValid is true, and just before the 'beforeClose' to check
        // the value if the value is valid, return true and the keyboard will continue as it should
        // (close if not always open, etc). If the value is not valid, return false and clear the keyboard
        // value ( like this "keyboard.$preview.val('');" ), if desired. The validate function is called after
        // each input, the 'isClosing' value will be false; when the accept button is clicked,
        // 'isClosing' is true
        validate: function (keyboard, value, isClosing) {
            return true;
        }

    };

    // for checking combos
    $keyboard.comboRegex = /([`\'~\^\"ao])([a-z])/mig;

    // store current keyboard element; used by base.isCurrent()
    $keyboard.currentKeyboard = '';

    $('<!--[if lte IE 8]><script>jQuery("body").addClass("oldie");</script><![endif]--><!--[if IE]>' +
        '<script>jQuery("body").addClass("ie");</script><![endif]-->')
        .appendTo('body')
        .remove();
    $keyboard.msie = $('body').hasClass('oldie'); // Old IE flag, used for caret positioning
    $keyboard.allie = $('body').hasClass('ie');

    $keyboard.watermark = (typeof (document.createElement('input').placeholder) !== 'undefined');

    $keyboard.checkCaretSupport = function () {
        if (typeof $keyboard.checkCaret !== 'boolean') {
            // Check if caret position is saved when input is hidden or loses focus
            // (*cough* all versions of IE and I think Opera has/had an issue as well
            var $temp = $('<div style="height:0px;width:0px;overflow:hidden;position:fixed;top:0;left:-100px;">' +
                '<input type="text" value="testing"/></div>').prependTo('body'); // stop page scrolling
            $keyboard.caret($temp.find('input'), 3, 3);
            // Also save caret position of the input if it is locked
            $keyboard.checkCaret = $keyboard.caret($temp.find('input').hide().show()).start !== 3;
            $temp.remove();
        }
        return $keyboard.checkCaret;
    };

    $keyboard.caret = function ($el, param1, param2) {
        if (!$el || !$el.length || $el.is(':hidden') || $el.css('visibility') === 'hidden') {
            return {};
        }
        var start, end, txt, pos,
            kb = $el.data('keyboard'),
            noFocus = kb && kb.options.noFocus;
        if (!noFocus) {
            $el.focus();
        }
        // set caret position
        if (typeof param1 !== 'undefined') {
            // allow setting caret using ( $el, { start: x, end: y } )
            if (typeof param1 === 'object' && 'start' in param1 && 'end' in param1) {
                start = param1.start;
                end = param1.end;
            } else if (typeof param2 === 'undefined') {
                param2 = param1; // set caret using start position
            }
            // set caret using ( $el, start, end );
            if (typeof param1 === 'number' && typeof param2 === 'number') {
                start = param1;
                end = param2;
            } else if (param1 === 'start') {
                start = end = 0;
            } else if (typeof param1 === 'string') {
                // unknown string setting, move caret to end
                start = end = $el.val().length;
            }

            // *** SET CARET POSITION ***
            // modify the line below to adapt to other caret plugins
            return $el.caret(start, end, noFocus);
        }
        // *** GET CARET POSITION ***
        // modify the line below to adapt to other caret plugins
        pos = $el.caret();
        start = pos.start;
        end = pos.end;

        // *** utilities ***
        txt = ($el[0].value || $el.text() || '');
        return {
            start: start,
            end: end,
            // return selected text
            text: txt.substring(start, end),
            // return a replace selected string method
            replaceStr: function (str) {
                return txt.substring(0, start) + str + txt.substring(end, txt.length);
            }
        };
    };

    $.fn.keyboard = function (options) {
        return this.each(function () {
            if (!$(this).data('keyboard')) {
                /*jshint nonew:false */
                (new $.keyboard(this, options));
            }
        });
    };

    $.fn.getkeyboard = function () {
        return this.data('keyboard');
    };

    /* Copyright (c) 2010 C. F., Wong (<a href="http://cloudgen.w0ng.hk">Cloudgen Examplet Store</a>)
     * Licensed under the MIT License:
     * http://www.opensource.org/licenses/mit-license.php
     * Highly modified from the original
     */

    $.fn.caret = function (start, end, noFocus) {
        if (typeof this[0] === 'undefined' || this.is(':hidden') || this.css('visibility') === 'hidden') {
            return this;
        }
        var selRange, range, stored_range, txt, val,
            selection = document.selection,
            $el = this,
            el = $el[0],
            sTop = el.scrollTop,
            ss = false,
            supportCaret = true;
        try {
            ss = 'selectionStart' in el;
        } catch (err) {
            supportCaret = false;
        }
        if (supportCaret && typeof start !== 'undefined') {
            if (!/(email|number)/i.test(el.type)) {
                if (ss) {
                    el.selectionStart = start;
                    el.selectionEnd = end;
                } else {
                    selRange = el.createTextRange();
                    selRange.collapse(true);
                    selRange.moveStart('character', start);
                    selRange.moveEnd('character', end - start);
                    selRange.select();
                }
            }
            // must be visible or IE8 crashes; IE9 in compatibility mode works fine - issue #56
            if (!noFocus && ($el.is(':visible') || $el.css('visibility') !== 'hidden')) {
                el.focus();
            }
            el.scrollTop = sTop;
            return this;
        } else {
            if (/(email|number)/i.test(el.type)) {
                // fix suggested by raduanastase (https://github.com/Mottie/Keyboard/issues/105#issuecomment-40456535)
                start = end = $el.val().length;
            } else if (ss) {
                start = el.selectionStart;
                end = el.selectionEnd;
            } else if (selection) {
                if (el.nodeName === 'TEXTAREA') {
                    val = $el.val();
                    range = selection.createRange();
                    stored_range = range.duplicate();
                    stored_range.moveToElementText(el);
                    stored_range.setEndPoint('EndToEnd', range);
                    // thanks to the awesome comments in the rangy plugin
                    start = stored_range.text.replace(/\r/g, '\n').length;
                    end = start + range.text.replace(/\r/g, '\n').length;
                } else {
                    val = $el.val().replace(/\r/g, '\n');
                    range = selection.createRange().duplicate();
                    range.moveEnd('character', val.length);
                    start = (range.text === '' ? val.length : val.lastIndexOf(range.text));
                    range = selection.createRange().duplicate();
                    range.moveStart('character', -val.length);
                    end = range.text.length;
                }
            } else {
                // caret positioning not supported
                start = end = (el.value || '').length;
            }
            txt = (el.value || '');
            return {
                start: start,
                end: end,
                text: txt.substring(start, end),
                replace: function (str) {
                    return txt.substring(0, start) + str + txt.substring(end, txt.length);
                }
            };
        }
    };

    return $keyboard;

}));
