virtual-keyboard
Version:
Virtual Keyboard using jQuery UI
1,584 lines (1,452 loc) • 113 kB
JavaScript
/*! jQuery UI Virtual Keyboard v1.30.4 *//*
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.30.4';
// 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 () {
base.initialized = false;
base.isTextArea = base.el.nodeName.toLowerCase() === 'textarea';
base.isInput = base.el.nodeName.toLowerCase() === 'input';
// detect contenteditable
base.isContentEditable = !base.isTextArea &&
!base.isInput &&
base.el.isContentEditable;
var k, position, tmp,
kbcss = $keyboard.css,
kbevents = $keyboard.events;
if (
base.isInput &&
$.inArray((base.el.type || '').toLowerCase(), $keyboard.supportedInputTypes) < 0
) {
throw new TypeError('Input of type "' + base.el.type + '" is not supported; use type text, search, URL, tel or password');
}
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 = typeof $.fn.mousewheel === 'function';
// 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;
base.checkCaret = (o.lockInput || $keyboard.checkCaretSupport());
// disable problematic usePreview for contenteditable
if (base.isContentEditable) {
o.usePreview = false;
}
base.last = {
start: 0,
end: 0,
key: '',
val: '',
preVal: '',
layout: '',
virtual: true,
keyset: [false, false, false], // [shift, alt, meta]
wheel_$Keys: [],
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,
kbevents.inputRestricted
], function (i, callback) {
if (typeof o[callback] === 'function') {
// 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);
}
var bindings = 'keyup checkkeyboard mousedown touchstart ';
if (o.closeByClickEvent) {
bindings += 'click ';
}
// debounce bindings... see #542
tmp.bind(bindings.split(' ').join(base.namespace + ' '), function(e) {
clearTimeout(base.timer3);
base.timer3 = setTimeout(function() {
base.checkClose(e);
}, 1);
});
// 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.isUnavailable()) {
base.$el.addClass(kbcss.noKeyboard);
}
if (o.openOn) {
base.bindFocus();
}
// Add placeholder if not supported by the browser
if (
!base.watermark &&
base.getValue(base.$el) === '' &&
base.inPlaceholder !== '' &&
base.$el.attr('placeholder') !== ''
) {
// css watermark style (darker text)
base.$el.addClass(kbcss.placeholder);
base.setValue(base.inPlaceholder, base.$el);
}
base.$el.trigger(kbevents.kbInit, [base, base.el]);
// initialized with keyboard open
if (o.alwaysOpen) {
base.reveal();
}
base.initialized = true;
};
base.toggle = function () {
if (!base.hasKeyboard()) { return; }
var $toggle = base.$keyboard.find('.' + $keyboard.css.keyToggle),
locked = !base.enabled;
// prevent physical keyboard from working
base.preview.readonly = locked || base.options.lockInput;
// disable all buttons
base.$keyboard
.toggleClass($keyboard.css.keyDisabled, locked)
.find('.' + $keyboard.css.keyButton)
.not($toggle)
.attr('aria-disabled', locked)
.each(function() {
this.disabled = locked;
});
$toggle.toggleClass($keyboard.css.keyDisabled, locked);
// stop auto typing
if (locked && base.typing_options) {
base.typing_options.text = '';
}
// allow chaining
return base;
};
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.$preview.focus();
base.$keyboard.addClass(kbcss.hasFocus);
base.isCurrent(true);
base.isOpen = true;
};
base.isUnavailable = function() {
return (
base.$el.is(':disabled') || (
!base.options.activeOnReadonly &&
base.$el.attr('readonly') &&
!base.$el.hasClass($keyboard.css.locked)
)
);
};
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.hasKeyboard = function () {
return base.$keyboard && base.$keyboard.length > 0;
};
base.isVisible = function () {
return base.hasKeyboard() ? base.$keyboard.is(':visible') : false;
};
base.setFocus = function () {
var $el = base.$preview || base.$el;
if (!o.noFocus) {
$el.focus();
}
if (base.isContentEditable) {
$keyboard.setEditableCaret($el, base.last.start, base.last.end);
} else {
$keyboard.caret($el, base.last);
}
};
base.focusOn = function () {
if (!base || !base.el.active) {
// keyboard was destroyed
return;
}
if (!base.isVisible()) {
clearTimeout(base.timer);
base.reveal();
} else {
// keyboard already open, make it the current keyboard
base.setCurrent();
}
};
// add redraw method to make API more clear
base.redraw = function (layout) {
if (layout) {
// allow updating the layout by calling redraw
base.options.layout = layout;
}
// update keyboard after a layout change
if (base.$keyboard.length) {
base.last.preVal = '' + base.last.val;
base.saveLastChange();
base.setValue(base.last.val, base.$el);
base.removeKeyboard();
base.shiftActive = base.altActive = base.metaActive = false;
}
base.isOpen = o.alwaysOpen;
base.reveal(true);
return base;
};
base.reveal = function (redraw) {
var temp,
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)) {
// this closes previous keyboard when clicking another input - see #515
kb.close(kb.options.autoAccept ? 'true' : false);
}
});
// Don't open if disabled
if (base.isUnavailable()) {
return;
}
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(base.el.ownerDocument.body, base.$keyboard[0]))) {
base.startup();
}
// clear watermark
if (!base.watermark && base.getValue() === base.inPlaceholder) {
base.$el.removeClass(kbcss.placeholder);
base.setValue('', base.$el);
}
// save starting content, in case we cancel
base.originalContent = base.isContentEditable ?
base.$el.html() :
base.getValue(base.$el);
if (base.el !== base.preview && !base.isContentEditable) {
base.setValue(base.originalContent);
}
// disable/enable accept button
if (o.acceptValid && o.checkValidOnInit) {
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]);
}
if (
base.initialized ||
o.initialFocus ||
( !o.initialFocus && base.$el.hasClass($keyboard.css.initialFocus) )
) {
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.reposition();
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) {
temp = base.isContentEditable ? $keyboard.getEditableLength(base.el) : base.originalContent.length;
base.saveCaret(temp, temp);
}
// 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);
base.opening = false;
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 || base.$el.hasClass($keyboard.css.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 = (Object.prototype.toString.call(lang) === '[object Array]' ? lang[0] : lang);
base.language = lang;
lang = 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)
if (kblang[lang] && kblang[lang].comboRegex) {
base.regex = kblang[lang].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.hasKeyboard()) {
// 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 (typeof o.create === 'function') {
// 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();
}
// Add layout and laguage data-attibutes
base.$keyboard
.attr('data-' + kbcss.keyboard + '-layout', o.layout)
.attr('data-' + kbcss.keyboard + '-language', base.language);
base.$decBtn = base.$keyboard.find('.' + kbcss.keyPrefix + 'dec');
// add enter to allowed keys; fixes #190
if (o.enterNavigation || base.isTextArea) {
base.alwaysAllowed.push($keyboard.keyCodes.enter);
}
base.bindKeyboard();
base.$keyboard.appendTo(o.appendLocally ? base.$el.parent() : o.appendTo || 'body');
base.bindKeys();
// reposition keyboard on window resize
if (o.reposition && $.ui && $.ui.position && o.appendTo === 'body') {
$(window).bind('resize' + base.namespace, function () {
base.reposition();
});
}
};
base.reposition = function () {
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) {
base.position.of =
// get single target position
base.position.of ||
// OR target stored in element data (multiple targets)
base.$el.data('keyboardPosition') ||
// OR default @ element
base.$el;
base.position.collision = base.position.collision || 'flipfit flipfit';
base.position.at = o.usePreview ? o.position.at : o.position.at2;
if (base.isVisible()) {
base.$keyboard.position(base.position);
}
}
// make chainable
return base;
};
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];
// 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;
}
};
// Added in v1.26.8 to allow chaining of the caret function, e.g.
// keyboard.reveal().caret(4,5).insertText('test').caret('end');
base.caret = function(param1, param2) {
var result = $keyboard.caret(base.$preview, param1, param2),
wasSetCaret = result instanceof $;
// Caret was set, save last position & make chainable
if (wasSetCaret) {
base.saveCaret(result.start, result.end);
return base;
}
// return caret position if using .caret()
return result;
};
base.saveCaret = function (start, end, $el) {
if (base.isCurrent()) {
var p;
if (typeof start === 'undefined') {
// grab & save current caret position
p = $keyboard.caret($el || base.$preview);
} else {
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.saveLastChange = function (val) {
base.last.val = val || base.getValue(base.$preview || base.$el);
if (base.isContentEditable) {
base.last.elms = base.el.cloneNode(true);
}
};
base.setScroll = function () {
// Set scroll so caret & current text is in view
// needed for virtual keyboard typing, NOT manual typing - fixes #23
if (!base.isContentEditable && base.last.virtual) {
var scrollWidth, clientWidth, adjustment, direction,
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);
// prevent submitting content on form submission
base.$previewCopy[0].disabled = true;
if (!base.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 (base.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],
namespace = base.namespace + 'keybindings';
base.$preview
.unbind(base.namespace)
.bind('click' + namespace + ' touchstart' + 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' + namespace, function (e) {
if (o.lockInput) {
return false;
}
if (!base.isCurrent()) {
return;
}
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);
// check, that keypress wasn't rise by functional key
// space is first typing symbol in UTF8 table
if (k < keyCodes.space) { //see #549
return;
}
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]);
}
} 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();
}
if (typeof o.beforeInsert === 'function') {
base.insertText(base.last.key);
e.preventDefault();
}
base.checkMaxLength();
})
.bind('keyup' + 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.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.saveLastChange();
// don't alter "e" or the "keyup" event never finishes processing; fixes #552
var event = $.Event( $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
event.action = base.last.key;
base.$el.trigger(event, [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 (typeof o.change === 'function') {
event.type = $keyboard.events.inputChange;
o.change(event, base, base.el);
return false;
}
if (o.acceptValid && o.autoAcceptOnValid) {
if (
typeof o.validate === 'function' &&
o.validate(base, base.getValue(base.$preview))
) {
base.$preview.blur();
base.accept();
}
}
})
.bind('keydown' + namespace, function (e) {
base.last.keyPress = e.which;
// 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 || e.timeStamp === base.last.timeStamp) {
return !o.lockInput;
}
base.last.timeStamp = e.timeStamp; // fixes #659
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(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();
$(base.el.ownerDocument).trigger('checkkeyboard' + base.namespace);
}
base.setFocus();
});
// 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.bindButton = function(events, handler) {
var button = '.' + $keyboard.css.keyButton,
callback = function(e) {
e.stopPropagation();
// save closest keyboard wrapper/input to check in checkClose function
e.$target = $(this).closest('.' + $keyboard.css.keyboard + ', .' + $keyboard.css.input);
handler.call(this, e);
};
if ($.fn.on) {
// jQuery v1.7+
base.$keyboard.on(events, button, callback);
} else if ($.fn.delegate) {
// jQuery v1.4.2 - 3.0.0
base.$keyboard.delegate(button, events, callback);
}
return base;
};
base.unbindButton = function(namespace) {
if ($.fn.off) {
// jQuery v1.7+
base.$keyboard.off(namespace);
} else if ($.fn.undelegate) {
// jQuery v1.4.2 - 3.0.0 (namespace only added in v1.6)
base.$keyboard.undelegate('.' + $keyboard.css.keyButton, namespace);
}
return base;
};
base.bindKeys = function () {
var kbcss = $keyboard.css;
base
.unbindButton(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
.bindButton('mouseenter mouseleave touchstart '.split(' ').join(base.namespace + ' '), function (e) {
if ((o.alwaysOpen || o.userClosed) && e.type !== 'mouseleave' && !base.isCurrent()) {
base.reveal();
base.setFocus();
}
if (!base.isCurrent() || this.disabled) {
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 = [];
$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
.bindButton(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') || this.disabled) {
return false;
}
var action,
last = base.last,
$key = $(this),
// 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
// target mousewheel selected key
$key = last.wheel_$Keys.length && last.wheelIndex > -1 ? last.wheel_$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;
last.$key = $key;
last.key = $key.attr('data-value');
last.keyPress = '';
// Start caret in IE when not focused (happens with each virtual keyboard button click
base.setFocus();
if (/^meta/.test(action)) {
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 && typeof $keyboard.keyaction[action] === 'function') {
// stop processing if action returns false (close & cancel)
if ($keyboard.keyaction[action](base, this, e) === false) {
return false;
}
action = null; // prevent inserting action name
}
// stop processing if keyboard closed and keyaction did not return false - see #536
if (!base.hasKeyboard()) {
return false;
}
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 = $.extend({}, e, $.Event($keyboard.events.kbChange));
e.target = base.el;
e.action = last.key;
base.$el.trigger(e, [base, base.el]);
last.preVal = '' + last.val;
base.saveLastChange();
if (typeof o.change === 'function') {
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
.bindButton('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!
if (o.acceptValid && o.autoAcceptOnValid) {
if (
typeof o.validate === 'function' &&
o.validate(base, base.getValue())
) {
base.$preview.blur();
base.accept();
}
}
return false;
})
// prevent form submits when keyboard is bound locally - issue #64
.bindButton('click' + base.namespace, function () {
return false;
})
// Allow mousewheel to scroll through other keysets of the same (non-action) key
.bindButton('mousewheel' + base.namespace, base.throttleEvent(function (e, delta) {
var $btn = $(this);
// no mouse repeat for action keys (shift, ctrl, alt, meta, etc)
if (!$btn || $btn.hasClass(kbcss.keyAction) || base.last.wheel_$Keys[0] !== this) {
return;
}
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;
$btn.html(txt[n]);
return false;
}
}, 30))
.bindButton('mousedown touchstart '.split(' ').join(base.namespace + 'kb '), function () {
var $btn = $(this);
// no mouse repeat for action keys (shift, ctrl, alt, meta, etc)
if (
!$btn || (
$btn.hasClass(kbcss.keyAction) &&
// mouse repeated action key exceptions
!$btn.is('.' + kbcss.keyPrefix + ('tab bksp space enter'.split(' ').join(',.' + kbcss.keyPrefix)))
)
) {
return;
}
if (o.repeatRate !== 0) {
// save the key, make sure we are repeating the right one (fast typers)
base.mouseRepeat = [true, $btn];
setTimeout(function () {
// don't repeat keys if it is disabled - see #431
if (base && base.mouseRepeat[0] && base.mouseRepeat[1] === $btn && !$btn[0].disabled) {
base.repeatKey($btn);
}
}, o.repeatDelay);
}
return false;
});
};
// No call on tailing event
base.throttleEvent = function(cb, time) {
var interm;
return function() {
if (!interm) {
cb.apply(this, arguments);
interm = true;
setTimeout(function() {
interm = false;
}, time);
}
};
};
base.execCommand = function(cmd, str) {
base.el.ownerDocument.execCommand(cmd, false, str);
base.el.normalize();
if (o.reposition) {
base.reposition();
}
};
base.getValue = function ($el) {
$el = $el || base.$preview;
return $el[base.isContentEditable ? 'text' : 'val']();
};
base.setValue = function (txt, $el) {
$el = $el || base.$preview;
if (base.isContentEditable) {
if (txt !== $el.text()) {
$keyboard.replaceContent($el, txt);
base.saveCaret();
}
} else {
$el.val(txt);
}
return base;
};
// Insert text at caret/selection - thanks to Derek Wickwire for fixing this up!
base.insertText = function (txt) {
if (!base.$preview) { return base; }
if (typeof o.beforeInsert === 'function') {
txt = o.beforeInsert(base.last.event, base, base.el, txt);
}
if (typeof txt === 'undefined' || txt === false) {
base.last.key = '';
return base;
}
if (base.isContentEditable) {
return base.insertContentEditable(txt);
}
var t,
bksp = false,
isBksp = txt === '\b',
// use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE).
val = base.getValue(),
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.isTextArea) {
// This makes sure the caret moves to the next line after clicking on enter (manual typing works fine)
if ($keyboard.msie && val.substring(pos.start, pos.start + 1) === '\n') {
pos.start += 1;
pos.end += 1;
}
}
t = pos.start;
if (txt === '{d}') {
txt = '';
pos.end += 1;
}
if (isBksp) {
txt = '';
bksp = isBksp && t === pos.end && t > 0;
}
val = val.substring(0, t - (bksp ? 1 : 0)) + txt + val.substring(pos.end);
t += bksp ? -1 : txt.length;
base.setValue(val);
base.saveCaret(t, t); // save caret in case of bksp
base.setScroll();
// see #506.. allow chaining of insertText
return base;
};
base.insertContentEditable = function (txt) {
base.$preview.focus();
base.execCommand('insertText', txt);
base.saveCaret();
return base;
};
// check max length
base.checkMaxLength = function () {
if (!base.$preview) { return; }
var start, caret,
val = base.getValue(),
len = base.isContentEditable ? $keyboard.getEditableLength(base.el) : val.length;
if (o.maxLength !== false && len > 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.setValue(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();
}
// allow chaining
return base;
};
// 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);
}
};
base.getKeySet = function () {
var sets = [];
if (base.altActive) {
sets.push('alt');
}
if (base.shiftActive) {
sets.push('shift');
}
if (base.metaActive) {
// base.metaActive contains the string name of the
// current meta keyset
sets.push(base.metaActive);
}
return sets.length ? sets.join('+') : 'normal';
};
// 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 (/\bmeta/.test(str)) {
base.metaActive = true;
base.showSet(str.match(/\bmeta[\w-]+/i)[0]);
} else {
base.metaActive = false;
base.showSet();
}
} else {
base.showSet(str);
}
// allow chaining
return base;
};
base.showSet = function (name) {
if (!base.hasKeyboard()) { return; }
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) {
// remove "-shift" and "-alt" from meta name if it exists
if (base.shiftActive) {
name = (name || '').replace('-shift', '');
}
if (base.altActive) {
name = (name || '').replace('-alt', '');
}
// the name attribute contains the meta set name '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).replace('--', '-'))
.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]);
if (o.reposition) {
base.reposition();
}
};
// check for key combos (dead keys)
base.checkCombos = function () {
// return val for close function
if ( !(
base.isVisible() || (
base.hasKeyboard() &&
base.$keyboard.hasClass( $keyboard.css.hasFocus )
)
) ) {
return base.getValue(base.$preview || base.$el);
}
var r, t, t2, repl,
// use base.$preview.val() instead of base.preview.value
// (val.length includes carriage returns in IE).
val = base.getValue(),
pos = $keyboard.caret(base.$preview),
layout = $keyboard.builtLayouts[base.layout],
max = base.isContentEditable ? $keyboard.getEditableLength(base.el) : val.length,
// save original content length
len = max;
// 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