1 /*! jQuery UI Virtual Keyboard v1.29.1 *//*
2 Author: Jeremy Satterfield
3 Maintained: Rob Garrison (Mottie on github)
4 Licensed under the MIT License
6 An on-screen virtual keyboard embedded within the browser window which
7 will popup when a specified entry field is focused. The user can then
8 type and preview their input before Accepting or Canceling.
10 This plugin adds default class names to match jQuery UI theme styling.
11 Bootstrap & custom themes may also be applied - See
12 https://github.com/Mottie/Keyboard#themes
16 Caret plugin (included)
18 jQuery UI (position utility only) & CSS theme
22 Please refer to https://github.com/Mottie/Keyboard/wiki
24 -----------------------------------------
25 Caret code modified from jquery.caret.1.02.js
26 Licensed under the MIT License:
27 http://www.opensource.org/licenses/mit-license.php
28 -----------------------------------------
30 /*jshint browser:true, jquery:true, unused:false */
31 /*global require:false, define:false, module:false */
32 ;(function (factory) {
33 if (typeof define === 'function' && define.amd) {
34 define(['jquery'], factory);
35 } else if (typeof module === 'object' && typeof module.exports === 'object') {
36 module.exports = factory(require('jquery'));
42 var $keyboard = $.keyboard = function (el, options) {
45 base.version = '1.29.1';
47 // Access to jQuery and DOM versions of element
51 // Add a reverse reference to the DOM object
52 base.$el.data('keyboard', base);
54 base.init = function () {
55 base.initialized = false;
57 kbcss = $keyboard.css,
58 kbevents = $keyboard.events;
59 base.settings = options || {};
60 // shallow copy position to prevent performance issues; see #357
61 if (options && options.position) {
62 position = $.extend({}, options.position);
63 options.position = null;
65 base.options = o = $.extend(true, {}, $keyboard.defaultOptions, options);
67 o.position = position;
68 options.position = position;
71 // keyboard is active (not destroyed);
72 base.el.active = true;
73 // unique keyboard namespace
74 base.namespace = '.keyboard' + Math.random().toString(16).slice(2);
75 // extension namespaces added here (to unbind listeners on base.$el upon destroy)
76 base.extensionNamespace = [];
77 // Shift and Alt key toggles, sets is true if a layout has more than one keyset
78 // used for mousewheel message
79 base.shiftActive = base.altActive = base.metaActive = base.sets = base.capsLock = false;
80 // Class names of the basic key set - meta keysets are handled by the keyname
81 base.rows = ['', '-shift', '-alt', '-alt-shift'];
83 base.inPlaceholder = base.$el.attr('placeholder') || '';
84 // html 5 placeholder/watermark
85 base.watermark = $keyboard.watermark && base.inPlaceholder !== '';
86 // convert mouse repeater rate (characters per second) into a time in milliseconds.
87 base.repeatTime = 1000 / (o.repeatRate || 20);
88 // delay in ms to prevent mousedown & touchstart from both firing events at the same time
89 o.preventDoubleEventTime = o.preventDoubleEventTime || 100;
90 // flag indication that a keyboard is open
92 // is mousewheel plugin loaded?
93 base.wheel = typeof $.fn.mousewheel === 'function';
94 // special character in regex that need to be escaped
95 base.escapeRegex = /[-\/\\^$*+?.()|[\]{}]/g;
96 base.isTextArea = base.el.nodeName.toLowerCase() === 'textarea';
97 base.isInput = base.el.nodeName.toLowerCase() === 'input';
98 // detect contenteditable
99 base.isContentEditable = !base.isTextArea &&
101 base.el.isContentEditable;
103 // keyCode of keys always allowed to be typed
104 k = $keyboard.keyCodes;
105 // base.alwaysAllowed = [20,33,34,35,36,37,38,39,40,45,46];
106 base.alwaysAllowed = [
120 // keyboard enabled; set to false on destroy
123 base.checkCaret = (o.lockInput || $keyboard.checkCaretSupport());
125 // disable problematic usePreview for contenteditable
126 if (base.isContentEditable) {
127 o.usePreview = false;
138 keyset: [false, false, false], // [shift, alt, meta]
143 // used when building the keyboard - [keyset element, row, index]
144 base.temp = ['', 0, 0];
149 kbevents.kbBeforeVisible,
152 kbevents.inputCanceled,
153 kbevents.inputAccepted,
154 kbevents.kbBeforeClose,
155 kbevents.inputRestricted
156 ], function (i, callback) {
157 if (typeof o[callback] === 'function') {
158 // bind callback functions within options to triggered events
159 base.$el.bind(callback + base.namespace + 'callbacks', o[callback]);
163 // Close with esc key & clicking outside
169 if (base.el.ownerDocument !== document) {
170 tmp = tmp.add(base.el.ownerDocument);
173 var bindings = 'keyup checkkeyboard mousedown touchstart ';
174 if (o.closeByClickEvent) {
175 bindings += 'click ';
177 // debounce bindings... see #542
178 tmp.bind(bindings.split(' ').join(base.namespace + ' '), function(e) {
179 clearTimeout(base.timer3);
180 base.timer3 = setTimeout(function() {
185 // Display keyboard on focus
187 .addClass(kbcss.input + ' ' + o.css.input)
189 'aria-haspopup': 'true',
193 // set lockInput if the element is readonly; or make the element readonly if lockInput is set
194 if (o.lockInput || base.el.readOnly) {
197 .addClass(kbcss.locked)
199 'readonly': 'readonly'
202 // add disabled/readonly class - dynamically updated on reveal
203 if (base.isUnavailable()) {
204 base.$el.addClass(kbcss.noKeyboard);
210 // Add placeholder if not supported by the browser
213 base.getValue(base.$el) === '' &&
214 base.inPlaceholder !== '' &&
215 base.$el.attr('placeholder') !== ''
217 // css watermark style (darker text)
218 base.$el.addClass(kbcss.placeholder);
219 base.setValue(base.inPlaceholder, base.$el);
222 base.$el.trigger(kbevents.kbInit, [base, base.el]);
224 // initialized with keyboard open
228 base.initialized = true;
231 base.toggle = function () {
232 if (!base.hasKeyboard()) { return; }
233 var $toggle = base.$keyboard.find('.' + $keyboard.css.keyToggle),
234 locked = !base.enabled;
235 // prevent physical keyboard from working
236 base.preview.readonly = locked || base.options.lockInput;
237 // disable all buttons
239 .toggleClass($keyboard.css.keyDisabled, locked)
240 .find('.' + $keyboard.css.keyButton)
242 .attr('aria-disabled', locked)
244 this.disabled = locked;
246 $toggle.toggleClass($keyboard.css.keyDisabled, locked);
248 if (locked && base.typing_options) {
249 base.typing_options.text = '';
255 base.setCurrent = function () {
256 var kbcss = $keyboard.css,
257 // close any "isCurrent" keyboard (just in case they are always open)
258 $current = $('.' + kbcss.isCurrent),
259 kb = $current.data('keyboard');
260 // close keyboard, if not self
261 if (!$.isEmptyObject(kb) && kb.el !== base.el) {
262 kb.close(kb.options.autoAccept ? 'true' : false);
264 $current.removeClass(kbcss.isCurrent);
265 // ui-keyboard-has-focus is applied in case multiple keyboards have
266 // alwaysOpen = true and are stacked
267 $('.' + kbcss.hasFocus).removeClass(kbcss.hasFocus);
269 base.$el.addClass(kbcss.isCurrent);
270 base.$keyboard.addClass(kbcss.hasFocus);
271 base.isCurrent(true);
275 base.isUnavailable = function() {
277 base.$el.is(':disabled') || (
278 !base.options.activeOnReadonly &&
279 base.$el.attr('readonly') &&
280 !base.$el.hasClass($keyboard.css.locked)
285 base.isCurrent = function (set) {
286 var cur = $keyboard.currentKeyboard || false;
288 cur = $keyboard.currentKeyboard = base.el;
289 } else if (set === false && cur === base.el) {
290 cur = $keyboard.currentKeyboard = '';
292 return cur === base.el;
295 base.hasKeyboard = function () {
296 return base.$keyboard && base.$keyboard.length > 0;
299 base.isVisible = function () {
300 return base.hasKeyboard() ? base.$keyboard.is(':visible') : false;
303 base.setFocus = function () {
304 var $el = base.$preview || base.$el;
308 if (base.isContentEditable) {
309 $keyboard.setEditableCaret($el, base.last.start, base.last.end);
311 $keyboard.caret($el, base.last);
315 base.focusOn = function () {
316 if (!base && base.el.active) {
317 // keyboard was destroyed
320 if (!base.isVisible()) {
321 clearTimeout(base.timer);
324 // keyboard already open, make it the current keyboard
329 // add redraw method to make API more clear
330 base.redraw = function (layout) {
332 // allow updating the layout by calling redraw
333 base.options.layout = layout;
335 // update keyboard after a layout change
336 if (base.$keyboard.length) {
338 base.last.preVal = '' + base.last.val;
339 base.saveLastChange();
340 base.setValue(base.last.val, base.$el);
342 base.removeKeyboard();
343 base.shiftActive = base.altActive = base.metaActive = false;
345 base.isOpen = o.alwaysOpen;
350 base.reveal = function (redraw) {
352 alreadyOpen = base.isOpen,
353 kbcss = $keyboard.css;
354 base.opening = !alreadyOpen;
355 // remove all 'extra' keyboards by calling close function
356 $('.' + kbcss.keyboard).not('.' + kbcss.alwaysOpen).each(function(){
357 var kb = $(this).data('keyboard');
358 if (!$.isEmptyObject(kb)) {
359 // this closes previous keyboard when clicking another input - see #515
360 kb.close(kb.options.autoAccept ? 'true' : false);
364 // Don't open if disabled
365 if (base.isUnavailable()) {
368 base.$el.removeClass(kbcss.noKeyboard);
370 // Unbind focus to prevent recursion - openOn may be empty if keyboard is opened externally
372 base.$el.unbind($.trim((o.openOn + ' ').split(/\s+/).join(base.namespace + ' ')));
375 // build keyboard if it doesn't exist; or attach keyboard if it was removed, but not cleared
376 if (!base.$keyboard || base.$keyboard &&
377 (!base.$keyboard.length || $.contains(base.el.ownerDocument.body, base.$keyboard[0]))) {
382 if (!base.watermark && base.getValue() === base.inPlaceholder) {
383 base.$el.removeClass(kbcss.placeholder);
384 base.setValue('', base.$el);
386 // save starting content, in case we cancel
387 base.originalContent = base.isContentEditable ?
389 base.getValue(base.$el);
390 if (base.el !== base.preview && !base.isContentEditable) {
391 base.setValue(base.originalContent);
394 // disable/enable accept button
395 if (o.acceptValid && o.checkValidOnInit) {
399 if (o.resetDefault) {
400 base.shiftActive = base.altActive = base.metaActive = false;
404 // beforeVisible event
405 if (!base.isVisible()) {
406 base.$el.trigger($keyboard.events.kbBeforeVisible, [base, base.el]);
411 ( !o.initialFocus && base.$el.hasClass($keyboard.css.initialFocus) )
415 // update keyboard - enabled or disabled?
419 base.$keyboard.show();
421 // adjust keyboard preview window width - save width so IE won't keep expanding (fix issue #6)
422 if (o.usePreview && $keyboard.msie) {
423 if (typeof base.width === 'undefined') {
424 base.$preview.hide(); // preview is 100% browser width in IE7, so hide the damn thing
425 base.width = Math.ceil(base.$keyboard.width()); // set input width to match the widest keyboard row
426 base.$preview.show();
428 base.$preview.width(base.width);
435 // get preview area line height
436 // add roughly 4px to get line height from font height, works well for font-sizes from 14-36px
437 // needed for textareas
438 base.lineHeight = parseInt(base.$preview.css('lineHeight'), 10) ||
439 parseInt(base.$preview.css('font-size'), 10) + 4;
442 temp = base.isContentEditable ? $keyboard.getEditableLength(base.el) : base.originalContent.length;
443 base.saveCaret(temp, temp);
447 if ($keyboard.allie) {
448 // sometimes end = 0 while start is > 0
449 if (base.last.end === 0 && base.last.start > 0) {
450 base.last.end = base.last.start;
452 // IE will have start -1, end of 0 when not focused (see demo: https://jsfiddle.net/Mottie/fgryQ/3/)
453 if (base.last.start < 0) {
454 // ensure caret is at the end of the text (needed for IE)
455 base.last.start = base.last.end = base.originalContent.length;
459 if (alreadyOpen || redraw) {
460 // restore caret position (userClosed)
461 $keyboard.caret(base.$preview, base.last);
462 base.opening = false;
466 // opening keyboard flag; delay allows switching between keyboards without immediately closing
468 base.timer2 = setTimeout(function () {
470 base.opening = false;
471 // Number inputs don't support selectionStart and selectionEnd
472 // Number/email inputs don't support selectionStart and selectionEnd
473 if (!/(number|email)/i.test(base.el.type) && !o.caretToEnd) {
474 // caret position is always 0,0 in webkit; and nothing is focused at this point... odd
475 // save caret position in the input to transfer it to the preview
476 // inside delay to get correct caret position
477 base.saveCaret(undef, undef, base.$el);
479 if (o.initialFocus || base.$el.hasClass($keyboard.css.initialFocus)) {
480 $keyboard.caret(base.$preview, base.last);
482 // save event time for keyboards with stayOpen: true
483 base.last.eventTime = new Date().getTime();
484 base.$el.trigger($keyboard.events.kbVisible, [base, base.el]);
485 base.timer = setTimeout(function () {
486 // get updated caret information after visible event - fixes #331
487 if (base) { // Check if base exists, this is a case when destroy is called, before timers fire
492 // return base to allow chaining in typing extension
496 base.updateLanguage = function () {
497 // change language if layout is named something like 'french-azerty-1'
498 var layouts = $keyboard.layouts,
499 lang = o.language || layouts[o.layout] && layouts[o.layout].lang &&
500 layouts[o.layout].lang || [o.language || 'en'],
501 kblang = $keyboard.language;
503 // some languages include a dash, e.g. 'en-gb' or 'fr-ca'
504 // allow o.language to be a string or array...
505 // array is for future expansion where a layout can be set for multiple languages
506 lang = (Object.prototype.toString.call(lang) === '[object Array]' ? lang[0] : lang);
507 base.language = lang;
508 lang = lang.split('-')[0];
510 // set keyboard language
511 o.display = $.extend(true, {},
513 kblang[lang] && kblang[lang].display || {},
514 base.settings.display
516 o.combos = $.extend(true, {},
518 kblang[lang] && kblang[lang].combos || {},
521 o.wheelMessage = kblang[lang] && kblang[lang].wheelMessage || kblang.en.wheelMessage;
522 // rtl can be in the layout or in the language definition; defaults to false
523 o.rtl = layouts[o.layout] && layouts[o.layout].rtl || kblang[lang] && kblang[lang].rtl || false;
525 // save default regex (in case loading another layout changes it)
526 if (kblang[lang] && kblang[lang].comboRegex) {
527 base.regex = kblang[lang].comboRegex;
529 // determine if US '.' or European ',' system being used
530 base.decimal = /^\./.test(o.display.dec);
532 .toggleClass('rtl', o.rtl)
533 .css('direction', o.rtl ? 'rtl' : '');
536 base.startup = function () {
537 var kbcss = $keyboard.css;
538 // ensure base.$preview is defined; but don't overwrite it if keyboard is always visible
539 if (!((o.alwaysOpen || o.userClosed) && base.$preview)) {
542 if (!base.hasKeyboard()) {
543 // custom layout - create a unique layout name based on the hash
544 if (o.layout === 'custom') {
545 o.layoutHash = 'custom' + base.customHash();
547 base.layout = o.layout === 'custom' ? o.layoutHash : o.layout;
548 base.last.layout = base.layout;
550 base.updateLanguage();
551 if (typeof $keyboard.builtLayouts[base.layout] === 'undefined') {
552 if (typeof o.create === 'function') {
553 // create must call buildKeyboard() function; or create it's own keyboard
554 base.$keyboard = o.create(base);
555 } else if (!base.$keyboard.length) {
556 base.buildKeyboard(base.layout, true);
559 base.$keyboard = $keyboard.builtLayouts[base.layout].$keyboard.clone();
560 base.$keyboard.data('keyboard', base);
561 if ((base.el.id || '') !== '') {
562 // add ID to keyboard for styling purposes
563 base.$keyboard.attr('id', base.el.id + $keyboard.css.idSuffix);
569 // Add layout and laguage data-attibutes
571 .attr('data-' + kbcss.keyboard + '-layout', o.layout)
572 .attr('data-' + kbcss.keyboard + '-language', base.language);
574 base.$decBtn = base.$keyboard.find('.' + kbcss.keyPrefix + 'dec');
575 // add enter to allowed keys; fixes #190
576 if (o.enterNavigation || base.isTextArea) {
577 base.alwaysAllowed.push($keyboard.keyCodes.enter);
582 base.$keyboard.appendTo(o.appendLocally ? base.$el.parent() : o.appendTo || 'body');
586 // reposition keyboard on window resize
587 if (o.reposition && $.ui && $.ui.position && o.appendTo === 'body') {
588 $(window).bind('resize' + base.namespace, function () {
595 base.reposition = function () {
596 base.position = $.isEmptyObject(o.position) ? false : o.position;
597 // position after keyboard is visible (required for UI position utility)
598 // and appropriately sized
599 if ($.ui && $.ui.position && base.position) {
601 // get single target position
603 // OR target stored in element data (multiple targets)
604 base.$el.data('keyboardPosition') ||
605 // OR default @ element
607 base.position.collision = base.position.collision || 'flipfit flipfit';
608 base.position.at = o.usePreview ? o.position.at : o.position.at2;
609 if (base.isVisible()) {
610 base.$keyboard.position(base.position);
617 base.makePreview = function () {
619 var indx, attrs, attr, removedAttr,
620 kbcss = $keyboard.css;
621 base.$preview = base.$el.clone(false)
622 .data('keyboard', base)
623 .removeClass(kbcss.placeholder + ' ' + kbcss.input)
624 .addClass(kbcss.preview + ' ' + o.css.input)
625 .attr('tabindex', '-1')
626 .show(); // for hidden inputs
627 base.preview = base.$preview[0];
629 // Switch the number input field to text so the caret positioning will work again
630 if (base.preview.type === 'number') {
631 base.preview.type = 'text';
634 // remove extraneous attributes.
635 removedAttr = /^(data-|id|aria-haspopup)/i;
636 attrs = base.$preview.get(0).attributes;
637 for (indx = attrs.length - 1; indx >= 0; indx--) {
638 attr = attrs[indx] && attrs[indx].name;
639 if (removedAttr.test(attr)) {
640 // remove data-attributes - see #351
641 base.preview.removeAttribute(attr);
644 // build preview container and append preview display
646 .addClass(kbcss.wrapper)
647 .append(base.$preview)
648 .prependTo(base.$keyboard);
650 base.$preview = base.$el;
651 base.preview = base.el;
655 // Added in v1.26.8 to allow chaining of the caret function, e.g.
656 // keyboard.reveal().caret(4,5).insertText('test').caret('end');
657 base.caret = function(param1, param2) {
658 var result = $keyboard.caret(base.$preview, param1, param2),
659 wasSetCaret = result instanceof $;
660 // Caret was set, save last position & make chainable
662 base.saveCaret(result.start, result.end);
665 // return caret position if using .caret()
669 base.saveCaret = function (start, end, $el) {
670 if (base.isCurrent()) {
672 if (typeof start === 'undefined') {
673 // grab & save current caret position
674 p = $keyboard.caret($el || base.$preview);
676 p = $keyboard.caret($el || base.$preview, start, end);
678 base.last.start = typeof start === 'undefined' ? p.start : start;
679 base.last.end = typeof end === 'undefined' ? p.end : end;
683 base.saveLastChange = function (val) {
684 base.last.val = val || base.getValue(base.$preview || base.$el);
685 if (base.isContentEditable) {
686 base.last.elms = base.el.cloneNode(true);
690 base.setScroll = function () {
691 // Set scroll so caret & current text is in view
692 // needed for virtual keyboard typing, NOT manual typing - fixes #23
693 if (!base.isContentEditable && base.last.virtual) {
695 var scrollWidth, clientWidth, adjustment, direction,
696 value = base.last.val.substring(0, Math.max(base.last.start, base.last.end));
698 if (!base.$previewCopy) {
700 base.$previewCopy = base.$preview.clone()
701 .removeAttr('id') // fixes #334
703 position: 'absolute',
708 .addClass($keyboard.css.inputClone);
709 // prevent submitting content on form submission
710 base.$previewCopy[0].disabled = true;
711 if (!base.isTextArea) {
712 // make input zero-width because we need an accurate scrollWidth
713 base.$previewCopy.css({
714 'white-space': 'pre',
719 // add clone inside of preview wrapper
720 base.$preview.after(base.$previewCopy);
722 // just slap that thing in there somewhere
723 base.$keyboard.prepend(base.$previewCopy);
727 if (base.isTextArea) {
728 // need the textarea scrollHeight, so set the clone textarea height to be the line height
730 .height(base.lineHeight)
732 // set scrollTop for Textarea
733 base.preview.scrollTop = base.lineHeight *
734 (Math.floor(base.$previewCopy[0].scrollHeight / base.lineHeight) - 1);
736 // add non-breaking spaces
737 base.$previewCopy.val(value.replace(/\s/g, '\xa0'));
739 // if scrollAdjustment option is set to "c" or "center" then center the caret
740 adjustment = /c/i.test(o.scrollAdjustment) ? base.preview.clientWidth / 2 : o.scrollAdjustment;
741 scrollWidth = base.$previewCopy[0].scrollWidth - 1;
743 // set initial state as moving right
744 if (typeof base.last.scrollWidth === 'undefined') {
745 base.last.scrollWidth = scrollWidth;
746 base.last.direction = true;
748 // if direction = true; we're scrolling to the right
749 direction = base.last.scrollWidth === scrollWidth ?
750 base.last.direction :
751 base.last.scrollWidth < scrollWidth;
752 clientWidth = base.preview.clientWidth - adjustment;
754 // set scrollLeft for inputs; try to mimic the inherit caret positioning + scrolling:
755 // hug right while scrolling right...
757 if (scrollWidth < clientWidth) {
758 base.preview.scrollLeft = 0;
760 base.preview.scrollLeft = scrollWidth - clientWidth;
763 // hug left while scrolling left...
764 if (scrollWidth >= base.preview.scrollWidth - clientWidth) {
765 base.preview.scrollLeft = base.preview.scrollWidth - adjustment;
766 } else if (scrollWidth - adjustment > 0) {
767 base.preview.scrollLeft = scrollWidth - adjustment;
769 base.preview.scrollLeft = 0;
773 base.last.scrollWidth = scrollWidth;
774 base.last.direction = direction;
779 base.bindFocus = function () {
781 // make sure keyboard isn't destroyed
782 // Check if base exists, this is a case when destroy is called, before timers have fired
783 if (base && base.el.active) {
784 base.$el.bind(o.openOn + base.namespace, function () {
787 // remove focus from element (needed for IE since blur doesn't seem to work)
788 if ($(':focus')[0] === base.el) {
795 base.bindKeyboard = function () {
797 keyCodes = $keyboard.keyCodes,
798 layout = $keyboard.builtLayouts[base.layout],
799 namespace = base.namespace + 'keybindings';
801 .unbind(base.namespace)
802 .bind('click' + namespace + ' touchstart' + namespace, function () {
803 if (o.alwaysOpen && !base.isCurrent()) {
806 // update last caret position after user click, use at least 150ms or it doesn't work in IE
807 base.timer2 = setTimeout(function () {
814 .bind('keypress' + namespace, function (e) {
818 if (!base.isCurrent()) {
822 var k = e.charCode || e.which,
823 // capsLock can only be checked while typing a-z
824 k1 = k >= keyCodes.A && k <= keyCodes.Z,
825 k2 = k >= keyCodes.a && k <= keyCodes.z,
826 str = base.last.key = String.fromCharCode(k);
827 // check, that keypress wasn't rise by functional key
828 // space is first typing symbol in UTF8 table
829 if (k < keyCodes.space) { //see #549
832 base.last.virtual = false;
834 base.last.$key = []; // not a virtual keyboard key
835 if (base.checkCaret) {
840 if (k !== keyCodes.capsLock && (k1 || k2)) {
841 base.capsLock = (k1 && !e.shiftKey) || (k2 && e.shiftKey);
842 // if shifted keyset not visible, then show it
843 if (base.capsLock && !base.shiftActive) {
844 base.shiftActive = true;
849 // restrict input - keyCode in keypress special keys:
850 // see http://www.asquare.net/javascript/tests/KeyCode.html
851 if (o.restrictInput) {
852 // allow navigation keys to work - Chrome doesn't fire a keypress event (8 = bksp)
853 if ((e.which === keyCodes.backSpace || e.which === 0) &&
854 $.inArray(e.keyCode, base.alwaysAllowed)) {
858 if ($.inArray(str, layout.acceptedKeys) === -1) {
860 // copy event object in case e.preventDefault() breaks when changing the type
861 evt = $.extend({}, e);
862 evt.type = $keyboard.events.inputRestricted;
863 base.$el.trigger(evt, [base, base.el]);
865 } else if ((e.ctrlKey || e.metaKey) &&
866 (e.which === keyCodes.A || e.which === keyCodes.C || e.which === keyCodes.V ||
867 (e.which >= keyCodes.X && e.which <= keyCodes.Z))) {
868 // Allow select all (ctrl-a), copy (ctrl-c), paste (ctrl-v) & cut (ctrl-x) &
869 // redo (ctrl-y)& undo (ctrl-z); meta key for mac
872 // Mapped Keys - allows typing on a regular keyboard and the mapped key is entered
873 // Set up a key in the layout as follows: 'm(a):label'; m = key to map, (a) = actual keyboard key
874 // to map to (optional), ':label' = title/tooltip (optional)
875 // example: \u0391 or \u0391(A) or \u0391:alpha or \u0391(A):alpha
876 if (layout.hasMappedKeys && layout.mappedKeys.hasOwnProperty(str)) {
877 base.last.key = layout.mappedKeys[str];
878 base.insertText(base.last.key);
881 if (typeof o.beforeInsert === 'function') {
882 base.insertText(base.last.key);
885 base.checkMaxLength();
888 .bind('keyup' + namespace, function (e) {
889 if (!base.isCurrent()) { return; }
890 base.last.virtual = false;
894 // Added a flag to prevent from tabbing into an input, keyboard opening, then adding the tab
895 // to the keyboard preview area on keyup. Sadly it still happens if you don't release the tab
896 // key immediately because keydown event auto-repeats
897 if (base.tab && !o.lockInput) {
898 base.shiftActive = e.shiftKey;
899 // when switching inputs, the tab keyaction returns false
900 var notSwitching = $keyboard.keyaction.tab(base);
910 // Escape will hide the keyboard
911 case keyCodes.escape:
913 base.close(o.autoAccept && o.autoAcceptOnEsc ? 'true' : false);
918 // throttle the check combo function because fast typers will have an incorrectly positioned caret
919 clearTimeout(base.throttled);
920 base.throttled = setTimeout(function () {
921 // fix error in OSX? see issue #102
922 if (base && base.isVisible()) {
927 base.checkMaxLength();
929 base.last.preVal = '' + base.last.val;
930 base.saveLastChange();
932 // don't alter "e" or the "keyup" event never finishes processing; fixes #552
933 var event = $.Event( $keyboard.events.kbChange );
934 // base.last.key may be empty string (shift, enter, tab, etc) when keyboard is first visible
935 // use e.key instead, if browser supports it
936 event.action = base.last.key;
937 base.$el.trigger(event, [base, base.el]);
939 // change callback is no longer bound to the input element as the callback could be
940 // called during an external change event with all the necessary parameters (issue #157)
941 if (typeof o.change === 'function') {
942 event.type = $keyboard.events.inputChange;
943 o.change(event, base, base.el);
946 if (o.acceptValid && o.autoAcceptOnValid) {
948 typeof o.validate === 'function' &&
949 o.validate(base, base.getValue(base.$preview))
951 base.$preview.blur();
956 .bind('keydown' + namespace, function (e) {
957 base.last.keyPress = e.which;
958 // ensure alwaysOpen keyboards are made active
959 if (o.alwaysOpen && !base.isCurrent()) {
962 // prevent tab key from leaving the preview window
963 if (e.which === keyCodes.tab) {
964 // allow tab to pass through - tab to next input/shift-tab for prev
968 if (o.lockInput || e.timeStamp === base.last.timeStamp) {
972 base.last.timeStamp = e.timeStamp; // fixes #659
973 base.last.virtual = false;
976 case keyCodes.backSpace:
977 $keyboard.keyaction.bksp(base, null, e);
982 $keyboard.keyaction.enter(base, null, e);
986 case keyCodes.capsLock:
987 base.shiftActive = base.capsLock = !base.capsLock;
992 // prevent ctrl-v/cmd-v
993 if (e.ctrlKey || e.metaKey) {
994 if (o.preventPaste) {
998 base.checkCombos(); // check pasted content
1003 .bind('mouseup touchend '.split(' ').join(namespace + ' '), function () {
1004 base.last.virtual = true;
1008 // prevent keyboard event bubbling
1009 base.$keyboard.bind('mousedown click touchstart '.split(' ').join(base.namespace + ' '), function (e) {
1010 e.stopPropagation();
1011 if (!base.isCurrent()) {
1013 $(base.el.ownerDocument).trigger('checkkeyboard' + base.namespace);
1018 // If preventing paste, block context menu (right click)
1019 if (o.preventPaste) {
1020 base.$preview.bind('contextmenu' + base.namespace, function (e) {
1023 base.$el.bind('contextmenu' + base.namespace, function (e) {
1030 base.bindButton = function(events, handler) {
1031 var button = '.' + $keyboard.css.keyButton,
1032 callback = function(e) {
1033 e.stopPropagation();
1034 // save closest keyboard wrapper/input to check in checkClose function
1035 e.$target = $(this).closest('.' + $keyboard.css.keyboard + ', .' + $keyboard.css.input);
1036 handler.call(this, e);
1040 base.$keyboard.on(events, button, callback);
1041 } else if ($.fn.delegate) {
1042 // jQuery v1.4.2 - 3.0.0
1043 base.$keyboard.delegate(button, events, callback);
1048 base.unbindButton = function(namespace) {
1051 base.$keyboard.off(namespace);
1052 } else if ($.fn.undelegate) {
1053 // jQuery v1.4.2 - 3.0.0 (namespace only added in v1.6)
1054 base.$keyboard.undelegate('.' + $keyboard.css.keyButton, namespace);
1059 base.bindKeys = function () {
1060 var kbcss = $keyboard.css;
1062 .unbindButton(base.namespace + ' ' + base.namespace + 'kb')
1063 // Change hover class and tooltip - moved this touchstart before option.keyBinding touchstart
1064 // to prevent mousewheel lag/duplication - Fixes #379 & #411
1065 .bindButton('mouseenter mouseleave touchstart '.split(' ').join(base.namespace + ' '), function (e) {
1066 if ((o.alwaysOpen || o.userClosed) && e.type !== 'mouseleave' && !base.isCurrent()) {
1070 if (!base.isCurrent() || this.disabled) {
1078 if (o.useWheel && base.wheel) {
1079 $keys = base.getLayers($this);
1080 txt = ($keys.length ? $keys.map(function () {
1081 return $(this).attr('data-value') || '';
1083 .get() : '') || [$this.text()];
1084 last.wheel_$Keys = $keys;
1085 last.wheelLayers = txt;
1086 last.wheelIndex = $.inArray($this.attr('data-value'), txt);
1089 if ((type === 'mouseenter' || type === 'touchstart') && base.el.type !== 'password' &&
1090 !$this.hasClass(o.css.buttonDisabled)) {
1091 $this.addClass(o.css.buttonHover);
1092 if (o.useWheel && base.wheel) {
1093 $this.attr('title', function (i, t) {
1094 // show mouse wheel message
1095 return (base.wheel && t === '' && base.sets && txt.length > 1 && type !== 'touchstart') ?
1100 if (type === 'mouseleave') {
1101 // needed or IE flickers really bad
1102 $this.removeClass((base.el.type === 'password') ? '' : o.css.buttonHover);
1103 if (o.useWheel && base.wheel) {
1104 last.wheelIndex = 0;
1105 last.wheelLayers = [];
1106 last.wheel_$Keys = [];
1108 .attr('title', function (i, t) {
1109 return (t === o.wheelMessage) ? '' : t;
1111 .html($this.attr('data-html')); // restore original button text
1115 // keyBinding = 'mousedown touchstart' by default
1116 .bindButton(o.keyBinding.split(' ').join(base.namespace + ' ') + base.namespace + ' ' +
1117 $keyboard.events.kbRepeater, function (e) {
1119 // prevent errors when external triggers attempt to 'type' - see issue #158
1120 if (!base.$keyboard.is(':visible') || this.disabled) {
1126 // prevent mousedown & touchstart from both firing events at the same time - see #184
1127 timer = new Date().getTime();
1129 if (o.useWheel && base.wheel) {
1130 // get keys from other layers/keysets (shift, alt, meta, etc) that line up by data-position
1131 // target mousewheel selected key
1132 $key = last.wheel_$Keys.length && last.wheelIndex > -1 ? last.wheel_$Keys.eq(last.wheelIndex) : $key;
1134 action = $key.attr('data-action');
1135 if (timer - (last.eventTime || 0) < o.preventDoubleEventTime) {
1138 last.eventTime = timer;
1140 last.virtual = true;
1142 last.key = $key.attr('data-value');
1144 // Start caret in IE when not focused (happens with each virtual keyboard button click
1146 if (/^meta/.test(action)) {
1149 // keyaction is added as a string, override original action & text
1150 if (action === last.key && typeof $keyboard.keyaction[action] === 'string') {
1151 last.key = action = $keyboard.keyaction[action];
1152 } else if (action in $keyboard.keyaction && typeof $keyboard.keyaction[action] === 'function') {
1153 // stop processing if action returns false (close & cancel)
1154 if ($keyboard.keyaction[action](base, this, e) === false) {
1157 action = null; // prevent inserting action name
1159 // stop processing if keyboard closed and keyaction did not return false - see #536
1160 if (!base.hasKeyboard()) {
1163 if (typeof action !== 'undefined' && action !== null) {
1164 last.key = $(this).hasClass(kbcss.keyAction) ? action : last.key;
1165 base.insertText(last.key);
1166 if (!base.capsLock && !o.stickyShift && !e.shiftKey) {
1167 base.shiftActive = false;
1168 base.showSet($key.attr('data-name'));
1171 // set caret if caret moved by action function; also, attempt to fix issue #131
1172 $keyboard.caret(base.$preview, last);
1174 e = $.extend({}, e, $.Event($keyboard.events.kbChange));
1176 e.action = last.key;
1177 base.$el.trigger(e, [base, base.el]);
1178 last.preVal = '' + last.val;
1179 base.saveLastChange();
1181 if (typeof o.change === 'function') {
1182 e.type = $keyboard.events.inputChange;
1183 o.change(e, base, base.el);
1184 // return false to prevent reopening keyboard if base.accept() was called
1189 // using 'kb' namespace for mouse repeat functionality to keep it separate
1190 // I need to trigger a 'repeater.keyboard' to make it work
1191 .bindButton('mouseup' + base.namespace + ' ' + 'mouseleave touchend touchmove touchcancel '.split(' ')
1192 .join(base.namespace + 'kb '), function (e) {
1193 base.last.virtual = true;
1196 if (e.type === 'touchmove') {
1197 // if moving within the same key, don't stop repeating
1198 offset = $this.offset();
1199 offset.right = offset.left + $this.outerWidth();
1200 offset.bottom = offset.top + $this.outerHeight();
1201 if (e.originalEvent.touches[0].pageX >= offset.left &&
1202 e.originalEvent.touches[0].pageX < offset.right &&
1203 e.originalEvent.touches[0].pageY >= offset.top &&
1204 e.originalEvent.touches[0].pageY < offset.bottom) {
1207 } else if (/(mouseleave|touchend|touchcancel)/i.test(e.type)) {
1208 $this.removeClass(o.css.buttonHover); // needed for touch devices
1210 if (!o.noFocus && base.isCurrent() && base.isVisible()) {
1211 base.$preview.focus();
1213 if (base.checkCaret) {
1214 $keyboard.caret(base.$preview, base.last);
1217 base.mouseRepeat = [false, ''];
1218 clearTimeout(base.repeater); // make sure key repeat stops!
1219 if (o.acceptValid && o.autoAcceptOnValid) {
1221 typeof o.validate === 'function' &&
1222 o.validate(base, base.getValue())
1224 base.$preview.blur();
1230 // prevent form submits when keyboard is bound locally - issue #64
1231 .bindButton('click' + base.namespace, function () {
1234 // Allow mousewheel to scroll through other keysets of the same (non-action) key
1235 .bindButton('mousewheel' + base.namespace, base.throttleEvent(function (e, delta) {
1237 // no mouse repeat for action keys (shift, ctrl, alt, meta, etc)
1238 if (!$btn || $btn.hasClass(kbcss.keyAction) || base.last.wheel_$Keys[0] !== this) {
1241 if (o.useWheel && base.wheel) {
1242 // deltaY used by newer versions of mousewheel plugin
1243 delta = delta || e.deltaY;
1245 txt = base.last.wheelLayers || [];
1246 if (txt.length > 1) {
1247 n = base.last.wheelIndex + (delta > 0 ? -1 : 1);
1248 if (n > txt.length - 1) {
1257 base.last.wheelIndex = n;
1262 .bindButton('mousedown touchstart '.split(' ').join(base.namespace + 'kb '), function () {
1264 // no mouse repeat for action keys (shift, ctrl, alt, meta, etc)
1267 $btn.hasClass(kbcss.keyAction) &&
1268 // mouse repeated action key exceptions
1269 !$btn.is('.' + kbcss.keyPrefix + ('tab bksp space enter'.split(' ').join(',.' + kbcss.keyPrefix)))
1274 if (o.repeatRate !== 0) {
1275 // save the key, make sure we are repeating the right one (fast typers)
1276 base.mouseRepeat = [true, $btn];
1277 setTimeout(function () {
1278 // don't repeat keys if it is disabled - see #431
1279 if (base && base.mouseRepeat[0] && base.mouseRepeat[1] === $btn && !$btn[0].disabled) {
1280 base.repeatKey($btn);
1288 // No call on tailing event
1289 base.throttleEvent = function(cb, time) {
1293 cb.apply(this, arguments);
1295 setTimeout(function() {
1302 base.execCommand = function(cmd, str) {
1303 base.el.ownerDocument.execCommand(cmd, false, str);
1304 base.el.normalize();
1310 base.getValue = function ($el) {
1311 $el = $el || base.$preview;
1312 return $el[base.isContentEditable ? 'text' : 'val']();
1315 base.setValue = function (txt, $el) {
1316 $el = $el || base.$preview;
1317 if (base.isContentEditable) {
1318 if (txt !== $el.text()) {
1319 $keyboard.replaceContent($el, txt);
1328 // Insert text at caret/selection - thanks to Derek Wickwire for fixing this up!
1329 base.insertText = function (txt) {
1330 if (!base.$preview) { return base; }
1331 if (typeof o.beforeInsert === 'function') {
1332 txt = o.beforeInsert(base.last.event, base, base.el, txt);
1334 if (typeof txt === 'undefined' || txt === false) {
1338 if (base.isContentEditable) {
1339 return base.insertContentEditable(txt);
1343 isBksp = txt === '\b',
1344 // use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE).
1345 val = base.getValue(),
1346 pos = $keyboard.caret(base.$preview),
1347 len = val.length; // save original content length
1349 // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea
1350 // is still difficult
1351 // in IE, pos.end can be zero after input loses focus
1352 if (pos.end < pos.start) {
1353 pos.end = pos.start;
1355 if (pos.start > len) {
1356 pos.end = pos.start = len;
1359 if (base.isTextArea) {
1360 // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine)
1361 if ($keyboard.msie && val.substring(pos.start, pos.start + 1) === '\n') {
1368 if (txt === '{d}') {
1375 bksp = isBksp && t === pos.end && t > 0;
1377 val = val.substring(0, t - (bksp ? 1 : 0)) + txt + val.substring(pos.end);
1378 t += bksp ? -1 : txt.length;
1381 base.saveCaret(t, t); // save caret in case of bksp
1383 // see #506.. allow chaining of insertText
1387 base.insertContentEditable = function (txt) {
1388 base.$preview.focus();
1389 base.execCommand('insertText', txt);
1395 base.checkMaxLength = function () {
1396 if (!base.$preview) { return; }
1398 val = base.getValue(),
1399 len = base.isContentEditable ? $keyboard.getEditableLength(base.el) : val.length;
1400 if (o.maxLength !== false && len > o.maxLength) {
1401 start = $keyboard.caret(base.$preview).start;
1402 caret = Math.min(start, o.maxLength);
1404 // prevent inserting new characters when maxed #289
1406 val = base.last.val;
1407 caret = start - 1; // move caret back one
1409 base.setValue(val.substring(0, o.maxLength));
1410 // restore caret on change, otherwise it ends up at the end.
1411 base.saveCaret(caret, caret);
1413 if (base.$decBtn.length) {
1414 base.checkDecimal();
1420 // mousedown repeater
1421 base.repeatKey = function (key) {
1422 key.trigger($keyboard.events.kbRepeater);
1423 if (base.mouseRepeat[0]) {
1424 base.repeater = setTimeout(function () {
1426 base.repeatKey(key);
1428 }, base.repeatTime);
1432 base.getKeySet = function () {
1434 if (base.altActive) {
1437 if (base.shiftActive) {
1440 if (base.metaActive) {
1441 // base.metaActive contains the string name of the
1442 // current meta keyset
1443 sets.push(base.metaActive);
1445 return sets.length ? sets.join('+') : 'normal';
1448 // make it easier to switch keysets via API
1449 // showKeySet('shift+alt+meta1')
1450 base.showKeySet = function (str) {
1451 if (typeof str === 'string') {
1452 base.last.keyset = [base.shiftActive, base.altActive, base.metaActive];
1453 base.shiftActive = /shift/i.test(str);
1454 base.altActive = /alt/i.test(str);
1455 if (/\bmeta/.test(str)) {
1456 base.metaActive = true;
1457 base.showSet(str.match(/\bmeta[\w-]+/i)[0]);
1459 base.metaActive = false;
1469 base.showSet = function (name) {
1470 if (!base.hasKeyboard()) { return; }
1471 o = base.options; // refresh options
1472 var kbcss = $keyboard.css,
1473 prefix = '.' + kbcss.keyPrefix,
1474 active = o.css.buttonActive,
1476 toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0);
1477 if (!base.shiftActive) {
1478 base.capsLock = false;
1480 // check meta key set
1481 if (base.metaActive) {
1482 // remove "-shift" and "-alt" from meta name if it exists
1483 if (base.shiftActive) {
1484 name = (name || '').replace('-shift', '');
1486 if (base.altActive) {
1487 name = (name || '').replace('-alt', '');
1489 // the name attribute contains the meta set name 'meta99'
1490 key = (/^meta/i.test(name)) ? name : '';
1491 // save active meta keyset name
1493 key = (base.metaActive === true) ? '' : base.metaActive;
1495 base.metaActive = key;
1497 // if meta keyset doesn't have a shift or alt keyset, then show just the meta key set
1498 if ((!o.stickyShift && base.last.keyset[2] !== base.metaActive) ||
1499 ((base.shiftActive || base.altActive) &&
1500 !base.$keyboard.find('.' + kbcss.keySet + '-' + key + base.rows[toShow]).length)) {
1501 base.shiftActive = base.altActive = false;
1503 } else if (!o.stickyShift && base.last.keyset[2] !== base.metaActive && base.shiftActive) {
1504 // switching from meta key set back to default, reset shift & alt if using stickyShift
1505 base.shiftActive = base.altActive = false;
1507 toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0);
1508 key = (toShow === 0 && !base.metaActive) ? '-normal' : (key === '') ? '' : '-' + key;
1509 if (!base.$keyboard.find('.' + kbcss.keySet + key + base.rows[toShow]).length) {
1510 // keyset doesn't exist, so restore last keyset settings
1511 base.shiftActive = base.last.keyset[0];
1512 base.altActive = base.last.keyset[1];
1513 base.metaActive = base.last.keyset[2];
1517 .find(prefix + 'alt,' + prefix + 'shift,.' + kbcss.keyAction + '[class*=meta]')
1518 .removeClass(active)
1520 .find(prefix + 'alt')
1521 .toggleClass(active, base.altActive)
1523 .find(prefix + 'shift')
1524 .toggleClass(active, base.shiftActive)
1526 .find(prefix + 'lock')
1527 .toggleClass(active, base.capsLock)
1529 .find('.' + kbcss.keySet)
1532 .find('.' + (kbcss.keyAction + prefix + key).replace('--', '-'))
1535 // show keyset using inline-block ( extender layout will then line up )
1536 base.$keyboard.find('.' + kbcss.keySet + key + base.rows[toShow])[0].style.display = 'inline-block';
1537 if (base.metaActive) {
1538 base.$keyboard.find(prefix + base.metaActive)
1539 // base.metaActive contains the string "meta#" or false
1540 // without the !== false, jQuery UI tries to transition the classes
1541 .toggleClass(active, base.metaActive !== false);
1543 base.last.keyset = [base.shiftActive, base.altActive, base.metaActive];
1544 base.$el.trigger($keyboard.events.kbKeysetChange, [base, base.el]);
1550 // check for key combos (dead keys)
1551 base.checkCombos = function () {
1552 // return val for close function
1554 base.isVisible() || (
1555 base.hasKeyboard() &&
1556 base.$keyboard.hasClass( $keyboard.css.hasFocus )
1559 return base.getValue(base.$preview || base.$el);
1562 // use base.$preview.val() instead of base.preview.value
1563 // (val.length includes carriage returns in IE).
1564 val = base.getValue(),
1565 pos = $keyboard.caret(base.$preview),
1566 layout = $keyboard.builtLayouts[base.layout],
1567 max = base.isContentEditable ? $keyboard.getEditableLength(base.el) : val.length,
1568 // save original content length
1570 // return if val is empty; fixes #352
1572 // check valid on empty string - see #429
1573 if (o.acceptValid) {
1579 // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea
1580 // is still difficult
1581 // in IE, pos.end can be zero after input loses focus
1582 if (pos.end < pos.start) {
1583 pos.end = pos.start;
1585 if (pos.start > len) {
1586 pos.end = pos.start = len;
1588 // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine)
1589 if ($keyboard.msie && val.substring(pos.start, pos.start + 1) === '\n') {
1595 // keep 'a' and 'o' in the regex for ae and oe ligature (æ,œ)
1596 // thanks to KennyTM: http://stackoverflow.com/q/4275077
1597 // original regex /([`\'~\^\"ao])([a-z])/mig moved to $.keyboard.comboRegex
1598 if ($keyboard.msie) {
1599 // old IE may not have the caret positioned correctly, so just check the whole thing
1600 val = val.replace(base.regex, function (s, accent, letter) {
1601 return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s;
1603 // prevent combo replace error, in case the keyboard closes - see issue #116
1604 } else if (base.$preview.length) {
1605 // Modern browsers - check for combos from last two characters left of the caret
1606 t = pos.start - (pos.start - 2 >= 0 ? 2 : 0);
1607 // target last two characters
1608 $keyboard.caret(base.$preview, t, pos.end);
1610 t = $keyboard.caret(base.$preview);
1611 repl = function (txt) {
1612 return (txt || '').replace(base.regex, function (s, accent, letter) {
1613 return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s;
1618 // prevent error if caret doesn't return a function
1619 if (t && t.replaceStr && t2 !== t.text) {
1620 if (base.isContentEditable) {
1621 $keyboard.replaceContent(el, repl);
1623 base.setValue(t.replaceStr(t2));
1626 val = base.getValue();
1630 // check input restrictions - in case content was pasted
1631 if (o.restrictInput && val !== '') {
1632 t = layout.acceptedKeys.length;
1634 r = layout.acceptedKeysRegex;
1636 t2 = $.map(layout.acceptedKeys, function (v) {
1637 // escape any special characters
1638 return v.replace(base.escapeRegex, '\\$&');
1640 if (base.alwaysAllowed.indexOf($keyboard.keyCodes.enter) > -1) {
1641 t2.push('\\n'); // Fixes #686
1643 r = layout.acceptedKeysRegex = new RegExp('(' + t2.join('|') + ')', 'g');
1645 // only save matching keys
1650 // no valid characters
1656 // save changes, then reposition caret
1657 pos.start += max - len;
1658 pos.end += max - len;
1661 base.saveCaret(pos.start, pos.end);
1662 // set scroll to keep caret in view
1664 base.checkMaxLength();
1666 if (o.acceptValid) {
1669 return val; // return text, used for keyboard closing section
1672 // Toggle accept button classes, if validating
1673 base.checkValid = function () {
1674 var kbcss = $keyboard.css,
1675 $accept = base.$keyboard.find('.' + kbcss.keyPrefix + 'accept'),
1677 if (typeof o.validate === 'function') {
1678 valid = o.validate(base, base.getValue(), false);
1680 // toggle accept button classes; defined in the css
1682 .toggleClass(kbcss.inputInvalid, !valid)
1683 .toggleClass(kbcss.inputValid, valid)
1684 // update title to indicate that the entry is valid or invalid
1685 .attr('title', $accept.attr('data-title') + ' (' + o.display[valid ? 'valid' : 'invalid'] + ')');
1688 // Decimal button for num pad - only allow one (not used by default)
1689 base.checkDecimal = function () {
1690 // Check US '.' or European ',' format
1691 if ((base.decimal && /\./g.test(base.preview.value)) ||
1692 (!base.decimal && /\,/g.test(base.preview.value))) {
1695 'disabled': 'disabled',
1696 'aria-disabled': 'true'
1698 .removeClass(o.css.buttonHover)
1699 .addClass(o.css.buttonDisabled);
1702 .removeAttr('disabled')
1704 'aria-disabled': 'false'
1706 .addClass(o.css.buttonDefault)
1707 .removeClass(o.css.buttonDisabled);
1711 // get other layer values for a specific key
1712 base.getLayers = function ($el) {
1713 var kbcss = $keyboard.css,
1714 key = $el.attr('data-pos'),
1715 $keys = $el.closest('.' + kbcss.keyboard)
1716 .find('button[data-pos="' + key + '"]');
1717 return $keys.filter(function () {
1719 .find('.' + kbcss.keyText)
1725 // Go to next or prev inputs
1726 // goToNext = true, then go to next input; if false go to prev
1727 // isAccepted is from autoAccept option or true if user presses shift+enter
1728 base.switchInput = function (goToNext, isAccepted) {
1729 if (typeof o.switchInput === 'function') {
1730 o.switchInput(base, goToNext, isAccepted);
1732 // base.$keyboard may be an empty array - see #275 (apod42)
1733 if (base.$keyboard.length) {
1734 base.$keyboard.hide();
1738 all = $('button, input, select, textarea, a, [contenteditable]')
1741 indx = all.index(base.$el) + (goToNext ? 1 : -1);
1742 if (base.$keyboard.length) {
1743 base.$keyboard.show();
1745 if (indx > all.length - 1) {
1746 stopped = o.stopAtEnd;
1747 indx = 0; // go to first input
1750 stopped = o.stopAtEnd;
1751 indx = all.length - 1; // stop or go to last
1754 isAccepted = base.close(isAccepted);
1758 kb = all.eq(indx).data('keyboard');
1759 if (kb && kb.options.openOn.length) {
1762 all.eq(indx).focus();
1769 // Close the keyboard, if visible. Pass a status of true, if the content was accepted
1770 // (for the event trigger).
1771 base.close = function (accepted) {
1772 if (base.isOpen && base.$keyboard.length) {
1773 clearTimeout(base.throttled);
1774 var kbcss = $keyboard.css,
1775 kbevents = $keyboard.events,
1776 val = accepted ? base.checkCombos() : base.originalContent;
1777 // validate input if accepted
1778 if (accepted && typeof o.validate === 'function' && !o.validate(base, val, true)) {
1779 val = base.originalContent;
1781 if (o.cancelClose) {
1785 base.isCurrent(false);
1786 base.isOpen = o.alwaysOpen || o.userClosed;
1787 if (base.isContentEditable && !accepted) {
1788 // base.originalContent stores the HTML
1791 base.setValue(val, base.$el);
1794 .removeClass(kbcss.isCurrent + ' ' + kbcss.inputAutoAccepted)
1795 // add 'ui-keyboard-autoaccepted' to inputs - see issue #66
1796 .addClass((accepted || false) ? accepted === true ? '' : kbcss.inputAutoAccepted : '')
1797 // trigger default change event - see issue #146
1798 .trigger(kbevents.inputChange);
1799 // don't trigger an empty event - see issue #463
1800 if (!o.alwaysOpen) {
1801 // don't trigger beforeClose if keyboard is always open
1802 base.$el.trigger(kbevents.kbBeforeClose, [base, base.el, (accepted || false)]);
1804 // save caret after updating value (fixes userClosed issue with changing focus)
1805 $keyboard.caret(base.$preview, base.last);
1808 .trigger(((accepted || false) ? kbevents.inputAccepted : kbevents.inputCanceled), [base, base.el])
1809 .trigger((o.alwaysOpen) ? kbevents.kbInactive : kbevents.kbHidden, [base, base.el])
1812 // base is undefined if keyboard was destroyed - fixes #358
1814 // add close event time
1815 base.last.eventTime = new Date().getTime();
1816 if (!(o.alwaysOpen || o.userClosed && accepted === 'true') && base.$keyboard.length) {
1818 base.removeKeyboard();
1819 // rebind input focus - delayed to fix IE issue #72
1820 base.timer = setTimeout(function () {
1826 if (!base.watermark && base.el.value === '' && base.inPlaceholder !== '') {
1827 base.$el.addClass(kbcss.placeholder);
1828 base.setValue(base.inPlaceholder, base.$el);
1835 base.accept = function () {
1836 return base.close(true);
1839 base.checkClose = function (e) {
1843 var kbcss = $.keyboard.css,
1844 $target = e.$target || $(e.target).closest('.' + $keyboard.css.keyboard + ', .' + $keyboard.css.input);
1845 if (!$target.length) {
1846 $target = $(e.target);
1848 // needed for IE to allow switching between keyboards smoothly
1849 if ($target.length && $target.hasClass(kbcss.keyboard)) {
1850 var kb = $target.data('keyboard');
1851 // only trigger on self
1854 !kb.$el.hasClass(kbcss.isCurrent) &&
1855 kb.options.openOn &&
1861 base.escClose(e, $target);
1865 // callback functions called to check if the keyboard needs to be closed
1866 // e.g. on escape or clicking outside the keyboard
1867 base.escCloseCallback = {
1868 // keep keyboard open if alwaysOpen or stayOpen is true - fixes mutliple
1869 // always open keyboards or single stay open keyboard
1870 keepOpen: function() {
1871 return !base.isOpen;
1875 base.escClose = function (e, $el) {
1879 if (e && e.type === 'keyup') {
1880 return (e.which === $keyboard.keyCodes.escape && !o.ignoreEsc) ?
1881 base.close(o.autoAccept && o.autoAcceptOnEsc ? 'true' : false) :
1884 var shouldStayOpen = false,
1885 $target = $el.length && $el || $(e.target);
1886 $.each(base.escCloseCallback, function(i, callback) {
1887 if (typeof callback === 'function') {
1888 shouldStayOpen = shouldStayOpen || callback($target);
1891 if (shouldStayOpen) {
1894 // ignore autoaccept if using escape - good idea?
1895 if (!base.isCurrent() && base.isOpen || base.isOpen && $target[0] !== base.el) {
1896 // don't close if stayOpen is set; but close if a different keyboard is being opened
1897 if ((o.stayOpen || o.userClosed) && !$target.hasClass($keyboard.css.input)) {
1900 // stop propogation in IE - an input getting focus doesn't open a keyboard if one is already open
1901 if ($keyboard.allie) {
1904 if (o.closeByClickEvent) {
1905 // only close the keyboard if the user is clicking on an input or if they cause a click
1906 // event (touchstart/mousedown will not force the close with this setting)
1907 var name = $target[0] && $target[0].nodeName.toLowerCase();
1908 if (name === 'input' || name === 'textarea' || e.type === 'click') {
1909 base.close(o.autoAccept ? 'true' : false);
1912 // send 'true' instead of a true (boolean), the input won't get a 'ui-keyboard-autoaccepted'
1913 // class name - see issue #66
1914 base.close(o.autoAccept ? 'true' : false);
1919 // Build default button
1920 base.keyBtn = $('<button />')
1924 'aria-disabled': 'false',
1927 .addClass($keyboard.css.keyButton);
1929 // convert key names into a class name
1930 base.processName = function (name) {
1932 process = (name || '').replace(/[^a-z0-9-_]/gi, ''),
1933 len = process.length,
1935 if (len > 1 && name === process) {
1936 // return name if basic text
1939 // return character code sequence
1942 for (index = 0; index < len; index++) {
1944 // keep '-' and '_'... so for dash, we get two dashes in a row
1945 newName.push(/[a-z0-9-_]/i.test(n) ?
1946 (/[-_]/.test(n) && index !== 0 ? '' : n) :
1947 (index === 0 ? '' : '-') + n.charCodeAt(0)
1950 return newName.join('');
1955 base.processKeys = function (name) {
1957 // Don't split colons followed by //, e.g. https://; Fixes #555
1958 parts = name.split(/:(?!\/\/)/),
1959 htmlIndex = name.indexOf('</'),
1960 colonIndex = name.indexOf(':', name.indexOf('<')),
1966 if (htmlIndex > -1 && (colonIndex < 0 || colonIndex > htmlIndex)) {
1967 // html includes colons; see #701
1972 format 'key(A):Label_for_key_(ignore_parentheses_here)'
1973 'key' = key that is seen (can any character(s); but it might need to be escaped using '\'
1974 or entered as unicode '\u####'
1975 '(A)' = the actual key on the real keyboard to remap
1976 ':Label_for_key' ends up in the title/tooltip
1978 '\u0391(A):alpha', 'x(y):this_(might)_cause_problems
1979 or edge cases of ':(x)', 'x(:)', 'x(()' or 'x())'
1980 Enhancement (if I can get alt keys to work):
1981 A mapped key will include the mod key, e.g. 'x(alt-x)' or 'x(alt-shift-x)'
1983 if (/\(.+\)/.test(parts[0]) || /^:\(.+\)/.test(name) || /\([(:)]\)/.test(name)) {
1984 // edge cases 'x(:)', 'x(()' or 'x())'
1985 if (/\([(:)]\)/.test(name)) {
1986 tmp = parts[0].match(/([^(]+)\((.+)\)/);
1987 if (tmp && tmp.length) {
1990 data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
1992 // edge cases 'x(:)', ':(x)' or ':(:)'
1993 data.name = name.match(/([^(]+)/)[0];
1994 if (data.name === ':') {
1995 // ':(:):test' => parts = [ '', '(', ')', 'title' ] need to slice 1
1996 parts = parts.slice(1);
1999 // 'x(:):test' => parts = [ 'x(', ')', 'title' ] need to slice 2
2001 parts = parts.slice(2);
2003 data.title = parts.length ? parts.join(':') : '';
2006 // example: \u0391(A):alpha; extract 'A' from '(A)'
2007 data.map = name.match(/\(([^()]+?)\)/)[1];
2008 // remove '(A)', left with '\u0391:alpha'
2009 name = name.replace(/\(([^()]+)\)/, '');
2010 tmp = name.split(':');
2011 // get '\u0391' from '\u0391:alpha'
2012 if (tmp[0] === '') {
2014 parts = parts.slice(1);
2018 data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
2022 // corner case of '::;' reduced to ':;', split as ['', ';']
2023 if (name !== '' && parts[0] === '') {
2025 parts = parts.slice(1);
2027 data.name = parts[0];
2029 data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
2031 data.title = $.trim(data.title).replace(/_/g, ' ');
2036 // keyName = the name of the function called in $.keyboard.keyaction when the button is clicked
2037 // name = name added to key, or cross-referenced in the display options
2038 // base.temp[0] = keyset to attach the new button
2039 // regKey = true when it is not an action key
2040 base.addKey = function (keyName, action, regKey) {
2041 var keyClass, tmp, keys,
2043 txt = base.processKeys(regKey ? keyName : action),
2044 kbcss = $keyboard.css;
2046 if (!regKey && o.display[txt.name]) {
2047 keys = base.processKeys(o.display[txt.name]);
2048 // action contained in "keyName" (e.g. keyName = "accept",
2049 // action = "a" (use checkmark instead of text))
2050 keys.action = base.processKeys(keyName).name;
2052 // when regKey is true, keyName is the same as action
2054 keys.action = txt.name;
2057 data.name = base.processName(txt.name);
2058 if (keys.name !== '') {
2059 if (keys.map !== '') {
2060 $keyboard.builtLayouts[base.layout].mappedKeys[keys.map] = keys.name;
2061 $keyboard.builtLayouts[base.layout].acceptedKeys.push(keys.name);
2062 } else if (regKey) {
2063 $keyboard.builtLayouts[base.layout].acceptedKeys.push(keys.name);
2068 keyClass = data.name === '' ? '' : kbcss.keyPrefix + data.name;
2070 // Action keys will have the 'ui-keyboard-actionkey' class
2071 keyClass = kbcss.keyAction + ' ' + kbcss.keyPrefix + keys.action;
2073 // '\u2190'.length = 1 because the unicode is converted, so if more than one character,
2074 // add the wide class
2075 keyClass += (keys.name.length > 2 ? ' ' + kbcss.keyWide : '') + ' ' + o.css.buttonDefault;
2077 data.html = '<span class="' + kbcss.keyText + '">' +
2078 // this prevents HTML from being added to the key
2079 keys.name.replace(/[\u00A0-\u9999]/gim, function (i) {
2080 return '&#' + i.charCodeAt(0) + ';';
2084 data.$key = base.keyBtn
2087 'data-value': regKey ? keys.name : keys.action, // value
2088 'data-name': keys.action,
2089 'data-pos': base.temp[1] + ',' + base.temp[2],
2090 'data-action': keys.action,
2091 'data-html': data.html
2093 // add 'ui-keyboard-' + data.name for all keys
2094 // (e.g. 'Bksp' will have 'ui-keyboard-bskp' class)
2095 // any non-alphanumeric characters will be replaced with
2096 // their decimal unicode value
2097 // (e.g. '~' is a regular key, class = 'ui-keyboard-126'
2098 // (126 is the unicode decimal value - same as ~)
2099 // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
2102 .appendTo(base.temp[0]);
2105 data.$key.attr('data-mapped', keys.map);
2107 if (keys.title || txt.title) {
2109 'data-title': txt.title || keys.title, // used to allow adding content to title
2110 'title': txt.title || keys.title
2114 if (typeof o.buildKey === 'function') {
2115 data = o.buildKey(base, data);
2116 // copy html back to attributes
2117 tmp = data.$key.html();
2118 data.$key.attr('data-html', tmp);
2123 base.customHash = function (layout) {
2124 /*jshint bitwise:false */
2125 var i, array, hash, character, len,
2128 // pass layout to allow for testing
2129 layout = typeof layout === 'undefined' ? o.customLayout : layout;
2130 // get all layout arrays
2131 for (array in layout) {
2132 if (layout.hasOwnProperty(array)) {
2133 arrays.push(layout[array]);
2137 merged = merged.concat.apply(merged, arrays).join(' ');
2138 // produce hash name - http://stackoverflow.com/a/7616484/145346
2140 len = merged.length;
2144 for (i = 0; i < len; i++) {
2145 character = merged.charCodeAt(i);
2146 hash = ((hash << 5) - hash) + character;
2147 hash = hash & hash; // Convert to 32bit integer
2152 base.buildKeyboard = function (name, internal) {
2153 // o.display is empty when this is called from the scramble extension (when alwaysOpen:true)
2154 if ($.isEmptyObject(o.display)) {
2155 // set keyboard language
2156 base.updateLanguage();
2158 var index, row, $row, currentSet,
2159 kbcss = $keyboard.css,
2161 layout = $keyboard.builtLayouts[name || base.layout || o.layout] = {
2165 acceptedKeys = layout.acceptedKeys = o.restrictInclude ?
2166 ('' + o.restrictInclude).split(/\s+/) || [] :
2168 // using $layout temporarily to hold keyboard popup classnames
2169 $layout = kbcss.keyboard + ' ' + o.css.popup + ' ' + o.css.container +
2170 (o.alwaysOpen || o.userClosed ? ' ' + kbcss.alwaysOpen : ''),
2172 container = $('<div />')
2179 // allow adding "{space}" as an accepted key - Fixes #627
2180 index = $.inArray('{space}', acceptedKeys);
2182 acceptedKeys[index] = ' ';
2185 // verify layout or setup custom keyboard
2186 if ((internal && o.layout === 'custom') || !$keyboard.layouts.hasOwnProperty(o.layout)) {
2187 o.layout = 'custom';
2188 $layout = $keyboard.layouts.custom = o.customLayout || {
2189 'normal': ['{cancel}']
2192 $layout = $keyboard.layouts[internal ? o.layout : name || base.layout || o.layout];
2195 // Main keyboard building loop
2196 $.each($layout, function (set, keySet) {
2197 // skip layout name & lang settings
2198 if (set !== '' && !/^(name|lang|rtl)$/i.test(set)) {
2199 // keep backwards compatibility for change from default to normal naming
2200 if (set === 'default') {
2205 .attr('name', set) // added for typing extension
2206 .addClass(kbcss.keySet + ' ' + kbcss.keySet + '-' + set)
2207 .appendTo(container)
2208 .toggle(set === 'normal');
2210 for (row = 0; row < keySet.length; row++) {
2211 // remove extra spaces before spliting (regex probably could be improved)
2212 currentSet = $.trim(keySet[row]).replace(/\{(\.?)[\s+]?:[\s+]?(\.?)\}/g, '{$1:$2}');
2213 base.buildRow($row, row, currentSet.split(/\s+/), acceptedKeys);
2214 $row.find('.' + kbcss.keyButton + ',.' + kbcss.keySpacer)
2216 .after('<br class="' + kbcss.endRow + '"/>');
2224 layout.hasMappedKeys = !($.isEmptyObject(layout.mappedKeys));
2225 layout.$keyboard = container;
2229 base.buildRow = function ($row, row, keys, acceptedKeys) {
2230 var t, txt, key, isAction, action, margin,
2231 kbcss = $keyboard.css;
2232 for (key = 0; key < keys.length; key++) {
2233 // used by addKey function
2234 base.temp = [$row, row, key];
2237 // ignore empty keys
2238 if (keys[key].length === 0) {
2242 // process here if it's an action key
2243 if (/^\{\S+\}$/.test(keys[key])) {
2244 action = keys[key].match(/^\{(\S+)\}$/)[1];
2245 // add active class if there are double exclamation points in the name
2246 if (/\!\!/.test(action)) {
2247 action = action.replace('!!', '');
2252 if (/^sp:((\d+)?([\.|,]\d+)?)(em|px)?$/i.test(action)) {
2253 // not perfect globalization, but allows you to use {sp:1,1em}, {sp:1.2em} or {sp:15px}
2254 margin = parseFloat(action
2256 .match(/^sp:((\d+)?([\.|,]\d+)?)(em|px)?$/i)[1] || 0
2258 $('<span class="' + kbcss.keyText + '"></span>')
2259 // previously {sp:1} would add 1em margin to each side of a 0 width span
2260 // now Firefox doesn't seem to render 0px dimensions, so now we set the
2261 // 1em margin x 2 for the width
2262 .width((action.match(/px/i) ? margin + 'px' : (margin * 2) + 'em'))
2263 .addClass(kbcss.keySpacer)
2268 if (/^empty(:((\d+)?([\.|,]\d+)?)(em|px)?)?$/i.test(action)) {
2269 margin = (/:/.test(action)) ? parseFloat(action
2271 .match(/^empty:((\d+)?([\.|,]\d+)?)(em|px)?$/i)[1] || 0
2274 .addKey('', ' ', true)
2275 .addClass(o.css.buttonDisabled + ' ' + o.css.buttonEmpty)
2276 .attr('aria-disabled', true)
2277 .width(margin ? (action.match('px') ? margin + 'px' : (margin * 2) + 'em') : '');
2282 if (/^meta[\w-]+\:?(\w+)?/i.test(action)) {
2284 .addKey(action.split(':')[0], action)
2285 .addClass(kbcss.keyHasActive);
2289 // switch needed for action keys with multiple names/shortcuts or
2290 // default will catch all others
2291 txt = action.split(':');
2292 switch (txt[0].toLowerCase()) {
2297 .addKey('accept', action)
2298 .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
2304 .addKey('alt', action)
2305 .addClass(kbcss.keyHasActive);
2310 base.addKey('bksp', action);
2316 .addKey('cancel', action)
2317 .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
2320 // toggle combo/diacritic key
2324 .addKey('combo', action)
2325 .addClass(kbcss.keyHasActive)
2326 .attr('title', function (indx, title) {
2327 // add combo key state to title
2328 return title + ' ' + o.display[o.useCombos ? 'active' : 'disabled'];
2330 .toggleClass(o.css.buttonActive, o.useCombos);
2333 // Decimal - unique decimal point (num pad layout)
2335 acceptedKeys.push((base.decimal) ? '.' : ',');
2336 base.addKey('dec', action);
2342 .addKey('enter', action)
2343 .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
2348 .addKey('lock', action)
2349 .addClass(kbcss.keyHasActive);
2355 .addKey('shift', action)
2356 .addClass(kbcss.keyHasActive);
2359 // Change sign (for num pad layout)
2361 acceptedKeys.push('-');
2362 base.addKey('sign', action);
2366 acceptedKeys.push(' ');
2367 base.addKey('space', action);
2372 base.addKey('tab', action);
2376 if ($keyboard.keyaction.hasOwnProperty(txt[0])) {
2378 .addKey(txt[0], action)
2379 .toggleClass(o.css.buttonAction + ' ' + kbcss.keyAction, isAction);
2386 // regular button (not an action key)
2388 base.addKey(t, t, true);
2393 base.removeBindings = function (namespace) {
2394 $(document).unbind(namespace);
2395 if (base.el.ownerDocument !== document) {
2396 $(base.el.ownerDocument).unbind(namespace);
2398 $(window).unbind(namespace);
2399 base.$el.unbind(namespace);
2402 base.removeKeyboard = function () {
2404 // base.$preview === base.$el when o.usePreview is false - fixes #442
2406 base.$preview.removeData('keyboard');
2408 base.$preview.unbind(base.namespace + 'keybindings');
2409 base.preview = null;
2410 base.$preview = null;
2411 base.$previewCopy = null;
2412 base.$keyboard.removeData('keyboard');
2413 base.$keyboard.remove();
2414 base.$keyboard = [];
2415 base.isOpen = false;
2416 base.isCurrent(false);
2419 base.destroy = function (callback) {
2421 kbcss = $keyboard.css,
2422 len = base.extensionNamespace.length,
2432 clearTimeout(base.timer);
2433 clearTimeout(base.timer2);
2434 clearTimeout(base.timer3);
2435 if (base.$keyboard.length) {
2436 base.removeKeyboard();
2438 if (base.options.openOn) {
2439 base.removeBindings(base.options.openOn);
2441 base.removeBindings(base.namespace);
2442 base.removeBindings(base.namespace + 'callbacks');
2443 for (index = 0; index < len; index++) {
2444 base.removeBindings(base.extensionNamespace[index]);
2446 base.el.active = false;
2450 .removeAttr('aria-haspopup')
2452 .removeData('keyboard');
2455 if (typeof callback === 'function') {
2463 }; // end $.keyboard definition
2465 // event.which & ASCII values
2466 $keyboard.keyCodes = {
2483 // event.which keyCodes (uppercase letters)
2490 // ASCII lowercase a & z
2496 // keyboard id suffix
2497 idSuffix: '_keyboard',
2498 // class name to set initial focus
2499 initialFocus: 'keyboard-init-focus',
2500 // element class names
2501 input: 'ui-keyboard-input',
2502 inputClone: 'ui-keyboard-preview-clone',
2503 wrapper: 'ui-keyboard-preview-wrapper',
2504 preview: 'ui-keyboard-preview',
2505 keyboard: 'ui-keyboard',
2506 keySet: 'ui-keyboard-keyset',
2507 keyButton: 'ui-keyboard-button',
2508 keyWide: 'ui-keyboard-widekey',
2509 keyPrefix: 'ui-keyboard-',
2510 keyText: 'ui-keyboard-text', // span with button text
2511 keyHasActive: 'ui-keyboard-hasactivestate',
2512 keyAction: 'ui-keyboard-actionkey',
2513 keySpacer: 'ui-keyboard-spacer', // empty keys
2514 keyToggle: 'ui-keyboard-toggle',
2515 keyDisabled: 'ui-keyboard-disabled',
2516 // Class for BRs with a div wrapper inside of contenteditable
2517 divWrapperCE: 'ui-keyboard-div-wrapper',
2519 locked: 'ui-keyboard-lockedinput',
2520 alwaysOpen: 'ui-keyboard-always-open',
2521 noKeyboard: 'ui-keyboard-nokeyboard',
2522 placeholder: 'ui-keyboard-placeholder',
2523 hasFocus: 'ui-keyboard-has-focus',
2524 isCurrent: 'ui-keyboard-input-current',
2525 // validation & autoaccept
2526 inputValid: 'ui-keyboard-valid-input',
2527 inputInvalid: 'ui-keyboard-invalid-input',
2528 inputAutoAccepted: 'ui-keyboard-autoaccepted',
2529 endRow: 'ui-keyboard-button-endrow' // class added to <br>
2532 $keyboard.events = {
2534 kbChange: 'keyboardChange',
2535 kbBeforeClose: 'beforeClose',
2536 kbBeforeVisible: 'beforeVisible',
2537 kbVisible: 'visible',
2538 kbInit: 'initialized',
2539 kbInactive: 'inactive',
2541 kbRepeater: 'repeater',
2542 kbKeysetChange: 'keysetChange',
2544 inputAccepted: 'accepted',
2545 inputCanceled: 'canceled',
2546 inputChange: 'change',
2547 inputRestricted: 'restricted'
2550 // Action key function list
2551 $keyboard.keyaction = {
2552 accept: function (base) {
2553 base.close(true); // same as base.accept();
2554 return false; // return false prevents further processing
2556 alt: function (base) {
2557 base.altActive = !base.altActive;
2560 bksp: function (base) {
2561 if (base.isContentEditable) {
2562 base.execCommand('delete');
2563 // save new caret position
2566 // the script looks for the '\b' string and initiates a backspace
2567 base.insertText('\b');
2570 cancel: function (base) {
2572 return false; // return false prevents further processing
2574 clear: function (base) {
2575 base.$preview[base.isContentEditable ? 'text' : 'val']('');
2576 if (base.$decBtn.length) {
2577 base.checkDecimal();
2580 combo: function (base) {
2581 var o = base.options,
2583 $combo = base.$keyboard.find('.' + $keyboard.css.keyPrefix + 'combo');
2586 .toggleClass(o.css.buttonActive, c)
2587 // update combo key state
2588 .attr('title', $combo.attr('data-title') + ' (' + o.display[c ? 'active' : 'disabled'] + ')');
2594 dec: function (base) {
2595 base.insertText((base.decimal) ? '.' : ',');
2597 del: function (base) {
2598 if (base.isContentEditable) {
2599 base.execCommand('forwardDelete');
2601 // the script looks for the '{d}' string and initiates a delete
2602 base.insertText('{d}');
2605 // resets to base keyset (deprecated because "default" is a reserved word)
2606 'default': function (base) {
2607 base.shiftActive = base.altActive = base.metaActive = false;
2610 // el is the pressed key (button) object; it is null when the real keyboard enter is pressed
2611 enter: function (base, el, e) {
2612 var o = base.options;
2613 // shift+enter in textareas
2615 // textarea, input & contenteditable - enterMod + shift + enter = accept,
2616 // then go to prev; base.switchInput(goToNext, autoAccept)
2617 // textarea & input - shift + enter = accept (no navigation)
2618 return (o.enterNavigation) ? base.switchInput(!e[o.enterMod], true) : base.close(true);
2620 // input only - enterMod + enter to navigate
2621 if (o.enterNavigation && (!base.isTextArea || e[o.enterMod])) {
2622 return base.switchInput(!e[o.enterMod], o.autoAccept ? 'true' : false);
2624 // pressing virtual enter button inside of a textarea - add a carriage return
2625 // e.target is span when clicking on text and button at other times
2626 if (base.isTextArea && $(e.target).closest('button').length) {
2627 // IE8 fix (space + \n) - fixes #71 thanks Blookie!
2628 base.insertText(($keyboard.msie ? ' ' : '') + '\n');
2630 if (base.isContentEditable && !o.enterNavigation) {
2631 base.execCommand('insertHTML', '<div><br class="' + $keyboard.css.divWrapperCE + '"></div>');
2632 // Using backspace on wrapped BRs will now shift the textnode inside of the wrapped BR
2633 // Although not ideal, the caret is moved after the block - see the wiki page for
2634 // more details: https://github.com/Mottie/Keyboard/wiki/Contenteditable#limitations
2635 // move caret after a delay to allow rendering of HTML
2636 setTimeout(function() {
2637 $keyboard.keyaction.right(base);
2643 lock: function (base) {
2644 base.last.keyset[0] = base.shiftActive = base.capsLock = !base.capsLock;
2647 left: function (base) {
2648 var p = $keyboard.caret(base.$preview);
2649 if (p.start - 1 >= 0) {
2650 // move both start and end of caret (prevents text selection) & save caret position
2651 base.last.start = base.last.end = p.start - 1;
2652 $keyboard.caret(base.$preview, base.last);
2656 meta: function (base, el) {
2658 base.metaActive = !$el.hasClass(base.options.css.buttonActive);
2659 base.showSet($el.attr('data-name'));
2661 next: function (base) {
2662 base.switchInput(true, base.options.autoAccept);
2665 // same as 'default' - resets to base keyset
2666 normal: function (base) {
2667 base.shiftActive = base.altActive = base.metaActive = false;
2670 prev: function (base) {
2671 base.switchInput(false, base.options.autoAccept);
2674 right: function (base) {
2675 var p = $keyboard.caret(base.$preview),
2676 len = base.isContentEditable ? $keyboard.getEditableLength(base.el) : base.getValue().length;
2677 if (p.end + 1 <= len) {
2678 // move both start and end of caret to end position
2679 // (prevents text selection) && save caret position
2680 base.last.start = base.last.end = p.end + 1;
2681 $keyboard.caret(base.$preview, base.last);
2685 shift: function (base) {
2686 base.last.keyset[0] = base.shiftActive = !base.shiftActive;
2689 sign: function (base) {
2690 if (/^[+-]?\d*\.?\d*$/.test(base.getValue())) {
2692 p = $keyboard.caret(base.$preview),
2693 val = base.getValue(),
2694 len = base.isContentEditable ? $keyboard.getEditableLength(base.el) : val.length;
2695 base.setValue(val * -1);
2696 caret = len - val.length;
2697 base.last.start = p.start + caret;
2698 base.last.end = p.end + caret;
2699 $keyboard.caret(base.$preview, base.last);
2703 space: function (base) {
2704 base.insertText(' ');
2706 tab: function (base) {
2707 var o = base.options;
2708 if (!base.isTextArea) {
2709 if (o.tabNavigation) {
2710 return base.switchInput(!base.shiftActive, true);
2711 } else if (base.isInput) {
2712 // ignore tab key in input
2716 base.insertText('\t');
2718 toggle: function (base) {
2719 base.enabled = !base.enabled;
2722 // *** Special action keys: NBSP & zero-width characters ***
2723 // Non-breaking space
2727 // Zero width non-joiner
2729 // Zero width joiner
2731 // Left-to-right Mark
2733 // Right-to-left Mark
2737 // Default keyboard layouts
2738 $keyboard.builtLayouts = {};
2739 $keyboard.layouts = {
2742 '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
2743 '{tab} a b c d e f g h i j [ ] \\',
2744 'k l m n o p q r s ; \' {enter}',
2745 '{shift} t u v w x y z , . / {shift}',
2746 '{accept} {space} {cancel}'
2749 '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
2750 '{tab} A B C D E F G H I J { } |',
2751 'K L M N O P Q R S : " {enter}',
2752 '{shift} T U V W X Y Z < > ? {shift}',
2753 '{accept} {space} {cancel}'
2758 '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
2759 '{tab} q w e r t y u i o p [ ] \\',
2760 'a s d f g h j k l ; \' {enter}',
2761 '{shift} z x c v b n m , . / {shift}',
2762 '{accept} {space} {cancel}'
2765 '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
2766 '{tab} Q W E R T Y U I O P { } |',
2767 'A S D F G H J K L : " {enter}',
2768 '{shift} Z X C V B N M < > ? {shift}',
2769 '{accept} {space} {cancel}'
2774 '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
2775 '{tab} q w e r t y u i o p [ ] \\',
2776 'a s d f g h j k l ; \' {enter}',
2777 '{shift} z x c v b n m , . / {shift}',
2778 '{accept} {alt} {space} {alt} {cancel}'
2781 '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
2782 '{tab} Q W E R T Y U I O P { } |',
2783 'A S D F G H J K L : " {enter}',
2784 '{shift} Z X C V B N M < > ? {shift}',
2785 '{accept} {alt} {space} {alt} {cancel}'
2788 '~ \u00a1 \u00b2 \u00b3 \u00a4 \u20ac \u00bc \u00bd \u00be \u2018 \u2019 \u00a5 \u00d7 {bksp}',
2789 '{tab} \u00e4 \u00e5 \u00e9 \u00ae \u00fe \u00fc \u00fa \u00ed \u00f3 \u00f6 \u00ab \u00bb \u00ac',
2790 '\u00e1 \u00df \u00f0 f g h j k \u00f8 \u00b6 \u00b4 {enter}',
2791 '{shift} \u00e6 x \u00a9 v b \u00f1 \u00b5 \u00e7 > \u00bf {shift}',
2792 '{accept} {alt} {space} {alt} {cancel}'
2795 '~ \u00b9 \u00b2 \u00b3 \u00a3 \u20ac \u00bc \u00bd \u00be \u2018 \u2019 \u00a5 \u00f7 {bksp}',
2796 '{tab} \u00c4 \u00c5 \u00c9 \u00ae \u00de \u00dc \u00da \u00cd \u00d3 \u00d6 \u00ab \u00bb \u00a6',
2797 '\u00c4 \u00a7 \u00d0 F G H J K \u00d8 \u00b0 \u00a8 {enter}',
2798 '{shift} \u00c6 X \u00a2 V B \u00d1 \u00b5 \u00c7 . \u00bf {shift}',
2799 '{accept} {alt} {space} {alt} {cancel}'
2804 '` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
2805 '{tab} q w f p g j l u y ; [ ] \\',
2806 '{bksp} a r s t d h n e i o \' {enter}',
2807 '{shift} z x c v b k m , . / {shift}',
2808 '{accept} {space} {cancel}'
2811 '~ ! @ # $ % ^ & * ( ) _ + {bksp}',
2812 '{tab} Q W F P G J L U Y : { } |',
2813 '{bksp} A R S T D H N E I O " {enter}',
2814 '{shift} Z X C V B K M < > ? {shift}',
2815 '{accept} {space} {cancel}'
2820 '` 1 2 3 4 5 6 7 8 9 0 [ ] {bksp}',
2821 '{tab} \' , . p y f g c r l / = \\',
2822 'a o e u i d h t n s - {enter}',
2823 '{shift} ; q j k x b m w v z {shift}',
2824 '{accept} {space} {cancel}'
2827 '~ ! @ # $ % ^ & * ( ) { } {bksp}',
2828 '{tab} " < > P Y F G C R L ? + |',
2829 'A O E U I D H T N S _ {enter}',
2830 '{shift} : Q J K X B M W V Z {shift}',
2831 '{accept} {space} {cancel}'
2846 $keyboard.language = {
2849 // check mark - same action as accept
2850 'a': '\u2714:Accept (Shift+Enter)',
2851 'accept': 'Accept:Accept (Shift+Enter)',
2852 // other alternatives \u2311
2853 'alt': 'Alt:\u2325 AltGr',
2854 // Left arrow (same as ←)
2855 'b': '\u232b:Backspace',
2856 'bksp': 'Bksp:Backspace',
2857 // big X, close - same action as cancel
2858 'c': '\u2716:Cancel (Esc)',
2859 'cancel': 'Cancel:Cancel (Esc)',
2862 'combo': '\u00f6:Toggle Combo Keys',
2863 // decimal point for num pad (optional), change '.' to ',' for European format
2865 // down, then left arrow - enter symbol
2866 'e': '\u23ce:Enter',
2868 'enter': 'Enter:Enter \u23ce',
2869 // left arrow (move caret)
2872 'lock': 'Lock:\u21ea Caps Lock',
2873 'next': 'Next \u21e8',
2874 'prev': '\u21e6 Prev',
2875 // right arrow (move caret)
2877 // thick hollow up arrow
2878 's': '\u21e7:Shift',
2879 'shift': 'Shift:Shift',
2880 // +/- sign for num pad
2881 'sign': '\u00b1:Change Sign',
2882 'space': '\u00a0:Space',
2883 // right arrow to bar (used since this virtual keyboard works with one directional tabs)
2885 // \u21b9 is the true tab symbol (left & right arrows)
2886 'tab': '\u21e5 Tab:Tab',
2887 // replaced by an image
2890 // added to titles of keys
2891 // accept key status when acceptValid:true
2893 'invalid': 'invalid',
2896 'disabled': 'disabled'
2899 // Message added to the key title while hovering, if the mousewheel plugin exists
2900 wheelMessage: 'Use mousewheel to see other keys',
2902 comboRegex: /([`\'~\^\"ao])([a-z])/mig,
2905 '`': { a: '\u00e0', A: '\u00c0', e: '\u00e8', E: '\u00c8', i: '\u00ec', I: '\u00cc', o: '\u00f2',
2906 O: '\u00d2', u: '\u00f9', U: '\u00d9', y: '\u1ef3', Y: '\u1ef2' },
2908 "'": { a: '\u00e1', A: '\u00c1', e: '\u00e9', E: '\u00c9', i: '\u00ed', I: '\u00cd', o: '\u00f3',
2909 O: '\u00d3', u: '\u00fa', U: '\u00da', y: '\u00fd', Y: '\u00dd' },
2911 '"': { a: '\u00e4', A: '\u00c4', e: '\u00eb', E: '\u00cb', i: '\u00ef', I: '\u00cf', o: '\u00f6',
2912 O: '\u00d6', u: '\u00fc', U: '\u00dc', y: '\u00ff', Y: '\u0178' },
2914 '^': { a: '\u00e2', A: '\u00c2', e: '\u00ea', E: '\u00ca', i: '\u00ee', I: '\u00ce', o: '\u00f4',
2915 O: '\u00d4', u: '\u00fb', U: '\u00db', y: '\u0177', Y: '\u0176' },
2917 '~': { a: '\u00e3', A: '\u00c3', e: '\u1ebd', E: '\u1ebc', i: '\u0129', I: '\u0128', o: '\u00f5',
2918 O: '\u00d5', u: '\u0169', U: '\u0168', y: '\u1ef9', Y: '\u1ef8', n: '\u00f1', N: '\u00d1' }
2923 $keyboard.defaultOptions = {
2924 // set this to ISO 639-1 language code to override language set by the layout
2925 // http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
2926 // language defaults to 'en' if not found
2930 // *** choose layout & positioning ***
2935 // optional - null (attach to input/textarea) or a jQuery object (attach elsewhere)
2939 // used when 'usePreview' is false (centers the keyboard at the bottom of the input/textarea)
2940 at2: 'center bottom'
2943 // allow jQuery position utility to reposition the keyboard on window resize
2946 // preview added above keyboard if true, original input/textarea used if false
2949 // if true, the keyboard will always be visible
2952 // give the preview initial focus when the keyboard becomes visible
2955 // avoid changing the focus (hardware keyboard probably won't work)
2958 // if true, keyboard will remain open even if the input loses focus, but closes on escape
2959 // or when another keyboard opens.
2962 // Prevents the keyboard from closing when the user clicks or presses outside the keyboard
2963 // the `autoAccept` option must also be set to true when this option is true or changes are lost
2966 // if true, keyboard will not close if you press escape.
2969 // if true, keyboard will only closed on click event instead of mousedown and touchstart
2970 closeByClickEvent: false,
2974 input: 'ui-widget-content ui-corner-all',
2975 // keyboard container
2976 container: 'ui-widget-content ui-widget ui-corner-all ui-helper-clearfix',
2977 // keyboard container extra class (same as container, but separate)
2980 buttonDefault: 'ui-state-default ui-corner-all',
2982 buttonHover: 'ui-state-hover',
2983 // Action keys (e.g. Accept, Cancel, Tab, etc); this replaces 'actionClass' option
2984 buttonAction: 'ui-state-active',
2985 // Active keys (e.g. shift down, meta keyset active, combo keys active)
2986 buttonActive: 'ui-state-active',
2987 // used when disabling the decimal button {dec} when a decimal exists in the input area
2988 buttonDisabled: 'ui-state-disabled',
2989 buttonEmpty: 'ui-keyboard-empty'
2992 // *** Useability ***
2993 // Auto-accept content when clicking outside the keyboard (popup will close)
2995 // Auto-accept content even if the user presses escape (only works if `autoAccept` is `true`)
2996 autoAcceptOnEsc: false,
2998 // Prevents direct input in the preview window when true
3001 // Prevent keys not in the displayed keyboard from being typed in
3002 restrictInput: false,
3003 // Additional allowed characters while restrictInput is true
3004 restrictInclude: '', // e.g. 'a b foo \ud83d\ude38'
3006 // Check input against validate function, if valid the accept button gets a class name of
3007 // 'ui-keyboard-valid-input'. If invalid, the accept button gets a class name of
3008 // 'ui-keyboard-invalid-input'
3010 // Auto-accept when input is valid; requires `acceptValid` set `true` & validate callback
3011 autoAcceptOnValid: false,
3012 // Check validation on keyboard initialization. If false, the "Accept" key state (color)
3013 // will not change to show if the content is valid, or not
3014 checkValidOnInit: true,
3016 // if acceptValid is true & the validate function returns a false, this option will cancel
3017 // a keyboard close only after the accept button is pressed
3020 // tab to go to next, shift-tab for previous (default behavior)
3021 tabNavigation: false,
3023 // enter for next input; shift+enter accepts content & goes to next
3024 // shift + 'enterMod' + enter ('enterMod' is the alt as set below) will accept content and go
3025 // to previous in a textarea
3026 enterNavigation: false,
3027 // mod key options: 'ctrlKey', 'shiftKey', 'altKey', 'metaKey' (MAC only)
3028 enterMod: 'altKey', // alt-enter to go to previous; shift-alt-enter to accept & go to previous
3030 // if true, the next button will stop on the last keyboard input/textarea; prev button stops at first
3031 // if false, the next button will wrap to target the first input/textarea; prev will go to the last
3034 // Set this to append the keyboard after the input/textarea (appended to the input/textarea parent).
3035 // This option works best when the input container doesn't have a set width & when the 'tabNavigation'
3037 appendLocally: false,
3038 // When appendLocally is false, the keyboard will be appended to this object
3041 // Wrap all <br>s inside of a contenteditable in a div; without wrapping, the caret
3042 // position will not be accurate
3045 // If false, the shift key will remain active until the next key is (mouse) clicked on; if true it will
3046 // stay active until pressed again
3049 // Prevent pasting content into the area
3050 preventPaste: false,
3052 // caret placed at the end of any text when keyboard becomes visible
3055 // caret stays this many pixels from the edge of the input while scrolling left/right;
3056 // use "c" or "center" to center the caret while scrolling
3057 scrollAdjustment: 10,
3059 // Set the max number of characters allowed in the input, setting it to false disables this option
3061 // allow inserting characters @ caret when maxLength is set
3064 // Mouse repeat delay - when clicking/touching a virtual keyboard key, after this delay the key will
3068 // Mouse repeat rate - after the repeatDelay, this is the rate (characters per second) at which the
3069 // key is repeated Added to simulate holding down a real keyboard key and having it repeat. I haven't
3070 // calculated the upper limit of this rate, but it is limited to how fast the javascript can process
3071 // the keys. And for me, in Firefox, it's around 20.
3074 // resets the keyboard to the default keyset when visible
3077 // Event (namespaced) on the input to reveal the keyboard. To disable it, just set it to ''.
3080 // enable the keyboard on readonly inputs
3081 activeOnReadonly: false,
3083 // Event (namepaced) for when the character is added to the input (clicking on the keyboard)
3084 keyBinding: 'mousedown touchstart',
3086 // enable/disable mousewheel functionality
3087 // enabling still depends on the mousewheel plugin
3090 // combos (emulate dead keys : http://en.wikipedia.org/wiki/Keyboard_layout#US-International)
3091 // if user inputs `a the script converts it to à , ^o becomes ô, etc.
3096 // commenting these out to reduce the size of the minified version
3097 // Callbacks - attach a function to any of these callbacks as desired
3098 initialized : function(e, keyboard, el) {},
3099 beforeVisible : function(e, keyboard, el) {},
3100 visible : function(e, keyboard, el) {},
3101 beforeInsert : function(e, keyboard, el, textToAdd) { return textToAdd; },
3102 change : function(e, keyboard, el) {},
3103 beforeClose : function(e, keyboard, el, accepted) {},
3104 accepted : function(e, keyboard, el) {},
3105 canceled : function(e, keyboard, el) {},
3106 restricted : function(e, keyboard, el) {},
3107 hidden : function(e, keyboard, el) {},
3108 // called instead of base.switchInput
3109 switchInput : function(keyboard, goToNext, isAccepted) {},
3110 // used if you want to create a custom layout or modify the built-in keyboard
3111 create : function(keyboard) { return keyboard.buildKeyboard(); },
3113 // build key callback
3114 buildKey : function( keyboard, data ) {
3118 isAction : [boolean] true if key is an action key
3119 name : [string] key class name suffix ( prefix = 'ui-keyboard-' );
3120 may include decimal ascii value of character
3121 value : [string] text inserted (non-action keys)
3122 title : [string] title attribute of key
3123 action : [string] keyaction name
3124 html : [string] HTML of the key; it includes a <span> wrapping the text
3125 // use to modify key HTML
3126 $key : [object] jQuery selector of key which is already appended to keyboard
3133 // this callback is called, if the acceptValid is true, and just before the 'beforeClose' to check
3134 // the value if the value is valid, return true and the keyboard will continue as it should
3135 // (close if not always open, etc). If the value is not valid, return false and clear the keyboard
3136 // value ( like this "keyboard.$preview.val('');" ), if desired. The validate function is called after
3137 // each input, the 'isClosing' value will be false; when the accept button is clicked,
3138 // 'isClosing' is true
3139 validate: function (/* keyboard, value, isClosing */) {
3145 // for checking combos
3146 $keyboard.comboRegex = /([`\'~\^\"ao])([a-z])/mig;
3148 // store current keyboard element; used by base.isCurrent()
3149 $keyboard.currentKeyboard = '';
3151 $('<!--[if lte IE 8]><script>jQuery("body").addClass("oldie");</script><![endif]--><!--[if IE]>' +
3152 '<script>jQuery("body").addClass("ie");</script><![endif]-->')
3155 $keyboard.msie = $('body').hasClass('oldie'); // Old IE flag, used for caret positioning
3156 $keyboard.allie = $('body').hasClass('ie');
3158 $keyboard.watermark = (typeof (document.createElement('input').placeholder) !== 'undefined');
3160 $keyboard.checkCaretSupport = function () {
3161 if (typeof $keyboard.checkCaret !== 'boolean') {
3162 // Check if caret position is saved when input is hidden or loses focus
3163 // (*cough* all versions of IE and I think Opera has/had an issue as well
3164 var $temp = $('<div style="height:0px;width:0px;overflow:hidden;position:fixed;top:0;left:-100px;">' +
3165 '<input type="text" value="testing"/></div>').prependTo('body'); // stop page scrolling
3166 $keyboard.caret($temp.find('input'), 3, 3);
3167 // Also save caret position of the input if it is locked
3168 $keyboard.checkCaret = $keyboard.caret($temp.find('input').hide().show()).start !== 3;
3171 return $keyboard.checkCaret;
3174 $keyboard.caret = function($el, param1, param2) {
3175 if (!$el || !$el.length || $el.is(':hidden') || $el.css('visibility') === 'hidden') {
3178 var start, end, txt, pos,
3179 kb = $el.data( 'keyboard' ),
3180 noFocus = kb && kb.options.noFocus,
3181 formEl = /(textarea|input)/i.test($el[0].nodeName);
3182 if (!noFocus) { $el.focus(); }
3183 // set caret position
3184 if (typeof param1 !== 'undefined') {
3185 // allow setting caret using ( $el, { start: x, end: y } )
3186 if (typeof param1 === 'object' && 'start' in param1 && 'end' in param1) {
3187 start = param1.start;
3189 } else if (typeof param2 === 'undefined') {
3190 param2 = param1; // set caret using start position
3192 // set caret using ( $el, start, end );
3193 if (typeof param1 === 'number' && typeof param2 === 'number') {
3196 } else if ( param1 === 'start' ) {
3198 } else if ( typeof param1 === 'string' ) {
3199 // unknown string setting, move caret to end
3200 start = end = 'end';
3203 // *** SET CARET POSITION ***
3204 // modify the line below to adapt to other caret plugins
3206 $el.caret( start, end, noFocus ) :
3207 $keyboard.setEditableCaret( $el, start, end );
3209 // *** GET CARET POSITION ***
3210 // modify the line below to adapt to other caret plugins
3212 // modify the line below to adapt to other caret plugins
3216 pos = $keyboard.getEditableCaret($el[0]);
3221 // *** utilities ***
3222 txt = formEl && $el[0].value || $el.text() || '';
3226 // return selected text
3227 text : txt.substring( start, end ),
3228 // return a replace selected string method
3229 replaceStr : function( str ) {
3230 return txt.substring( 0, start ) + str + txt.substring( end, txt.length );
3235 $keyboard.isTextNode = function(el) {
3236 return el && el.nodeType === 3;
3239 $keyboard.isBlock = function(el, node) {
3240 var win = el.ownerDocument.defaultView;
3242 node && node.nodeType === 1 && node !== el &&
3243 win.getComputedStyle(node).display === 'block'
3250 // Wrap all BR's inside of contenteditable
3251 $keyboard.wrapBRs = function(container) {
3252 var $el = $(container).find('br:not(.' + $keyboard.css.divWrapperCE + ')');
3254 $.each($el, function(i, el) {
3255 var len = el.parentNode.childNodes.length;
3257 // wrap BRs if not solo child
3259 // Or if BR is wrapped by a span
3260 len === 1 && !$keyboard.isBlock(container, el.parentNode)
3262 $(el).addClass($keyboard.css.divWrapperCE).wrap('<div>');
3268 $keyboard.getEditableCaret = function(container) {
3269 container = $(container)[0];
3270 if (!container.isContentEditable) { return {}; }
3272 options = ($(container).data('keyboard') || {}).options,
3273 doc = container.ownerDocument,
3274 range = doc.getSelection().getRangeAt(0),
3275 result = pathToNode(range.startContainer, range.startOffset),
3276 start = result.position;
3277 if (options.wrapBRs !== false) {
3278 $keyboard.wrapBRs(container);
3280 function pathToNode(endNode, offset) {
3285 nodes = $.makeArray(container.childNodes);
3287 function checkBlock(val) {
3290 txt += options && options.replaceCR || '\n';
3294 while (!done && nodes.length) {
3295 node = nodes.shift();
3296 if (node === endNode) {
3300 // Add one if previous sibling was a block node (div, p, etc)
3301 adjust = $keyboard.isBlock(container, node.previousSibling);
3304 if ($keyboard.isTextNode(node)) {
3305 position += done ? offset : node.length;
3306 txt += node.textContent;
3308 return {position: position, text: txt};
3310 } else if (!done && node.childNodes) {
3311 nodes = $.makeArray(node.childNodes).concat(nodes);
3313 // Add one if we're inside a block node (div, p, etc)
3314 // and previous sibling was a text node
3315 adjust = $keyboard.isTextNode(node.previousSibling) && $keyboard.isBlock(container, node);
3318 return {position: position, text: txt};
3320 // check of start and end are the same
3321 if (range.endContainer === range.startContainer && range.endOffset === range.startOffset) {
3325 result = pathToNode(range.endContainer, range.endOffset);
3326 end = result.position;
3327 text = result.text.substring(start, end);
3336 $keyboard.getEditableLength = function(container) {
3337 var result = $keyboard.setEditableCaret(container, 'getMax');
3338 // if not a number, the container is not a contenteditable element
3339 return typeof result === 'number' ? result : null;
3342 $keyboard.setEditableCaret = function(container, start, end) {
3343 container = $(container)[0];
3344 if (!container.isContentEditable) { return {}; }
3345 var doc = container.ownerDocument,
3346 range = doc.createRange(),
3347 sel = doc.getSelection(),
3348 options = ($(container).data('keyboard') || {}).options,
3352 result = findNode(start === 'getMax' ? 'end' : start);
3353 function findNode(offset) {
3354 if (offset === 'end') {
3355 // Set some value > content length; but return max
3356 offset = container.innerHTML.length;
3357 } else if (offset < 0) {
3366 nodes = $.makeArray(container.childNodes);
3367 function updateText(val) {
3368 txt += val ? options && options.replaceCR || '\n' : '';
3371 function checkDone(adj) {
3372 var val = position + adj;
3375 if (offset - val >= 0) {
3377 return offset - position <= 0;
3379 return offset - val <= 0;
3381 while (!done && nodes.length) {
3382 node = nodes.shift();
3383 // Add one if the previous sibling was a block node (div, p, etc)
3384 check = $keyboard.isBlock(container, node.previousSibling);
3385 if (updateText(check) && checkDone(check)) {
3388 // Add one if we're inside a block node (div, p, etc)
3389 check = $keyboard.isTextNode(node.previousSibling) && $keyboard.isBlock(container, node);
3390 if (updateText(check) && checkDone(check)) {
3393 if ($keyboard.isTextNode(node)) {
3394 txt += node.textContent;
3395 if (checkDone(node.length)) {
3396 check = offset - position === 0 && position - last >= 1 ? node.length : offset - position;
3404 } else if (!done && node.childNodes) {
3405 nodes = $.makeArray(node.childNodes).concat(nodes);
3408 return nodes.length ?
3409 {node: node, offset: offset - position, position: offset, text: txt} :
3410 // Offset is larger than content, return max
3411 {node: node, offset: node && node.length || 0, position: max, text: txt};
3414 s = result.position; // Adjust if start > content length
3415 if (start === 'getMax') {
3418 range.setStart(result.node, result.offset);
3419 // Only find end if > start and is defined... this allows passing
3420 // setEditableCaret(el, 'end') or setEditableCaret(el, 10, 'end');
3421 if (typeof end !== 'undefined' && end !== start) {
3422 result = findNode(end);
3425 e = result.position; // Adjust if end > content length
3426 range.setEnd(result.node, result.offset);
3427 text = s === e ? '' : result.text.substring(s, e);
3429 sel.removeAllRanges();
3430 sel.addRange(range);
3439 $keyboard.replaceContent = function (el, param) {
3442 type = typeof param,
3443 caret = $keyboard.getEditableCaret(el).start,
3446 while ((node = nodeStack.pop())) {
3447 if ($keyboard.isTextNode(node)) {
3448 if (type === 'function') {
3449 if (caret >= charIndex && caret <= charIndex + node.length) {
3450 node.textContent = param(node.textContent);
3452 } else if (type === 'string') {
3453 // maybe not the best method, but it works for simple changes
3454 str = param.substring(charIndex, charIndex + node.length);
3455 if (str !== node.textContent) {
3456 node.textContent = str;
3459 charIndex += node.length;
3460 } else if (node && node.childNodes) {
3461 i = node.childNodes.length;
3463 nodeStack.push(node.childNodes[i]);
3467 i = $keyboard.getEditableCaret(el);
3468 $keyboard.setEditableCaret(el, i.start, i.start);
3471 $.fn.keyboard = function (options) {
3472 return this.each(function () {
3473 if (!$(this).data('keyboard')) {
3474 /*jshint nonew:false */
3475 (new $.keyboard(this, options));
3480 $.fn.getkeyboard = function () {
3481 return this.data('keyboard');
3484 /* Copyright (c) 2010 C. F., Wong (<a href="http://cloudgen.w0ng.hk">Cloudgen Examplet Store</a>)
3485 * Licensed under the MIT License:
3486 * http://www.opensource.org/licenses/mit-license.php
3487 * Highly modified from the original
3490 $.fn.caret = function (start, end, noFocus) {
3492 typeof this[0] === 'undefined' ||
3493 this.is(':hidden') ||
3494 this.css('visibility') === 'hidden' ||
3495 !/(INPUT|TEXTAREA)/i.test(this[0].nodeName)
3499 var selRange, range, stored_range, txt, val,
3502 selection = el.ownerDocument.selection,
3503 sTop = el.scrollTop,
3505 supportCaret = true;
3507 ss = 'selectionStart' in el;
3509 supportCaret = false;
3511 if (supportCaret && typeof start !== 'undefined') {
3512 if (!/(email|number)/i.test(el.type)) {
3514 el.selectionStart = start;
3515 el.selectionEnd = end;
3517 selRange = el.createTextRange();
3518 selRange.collapse(true);
3519 selRange.moveStart('character', start);
3520 selRange.moveEnd('character', end - start);
3524 // must be visible or IE8 crashes; IE9 in compatibility mode works fine - issue #56
3525 if (!noFocus && ($el.is(':visible') || $el.css('visibility') !== 'hidden')) {
3528 el.scrollTop = sTop;
3531 if (/(email|number)/i.test(el.type)) {
3532 // fix suggested by raduanastase (https://github.com/Mottie/Keyboard/issues/105#issuecomment-40456535)
3533 start = end = $el.val().length;
3535 start = el.selectionStart;
3536 end = el.selectionEnd;
3537 } else if (selection) {
3538 if (el.nodeName.toUpperCase() === 'TEXTAREA') {
3540 range = selection.createRange();
3541 stored_range = range.duplicate();
3542 stored_range.moveToElementText(el);
3543 stored_range.setEndPoint('EndToEnd', range);
3544 // thanks to the awesome comments in the rangy plugin
3545 start = stored_range.text.replace(/\r/g, '\n').length;
3546 end = start + range.text.replace(/\r/g, '\n').length;
3548 val = $el.val().replace(/\r/g, '\n');
3549 range = selection.createRange().duplicate();
3550 range.moveEnd('character', val.length);
3551 start = (range.text === '' ? val.length : val.lastIndexOf(range.text));
3552 range = selection.createRange().duplicate();
3553 range.moveStart('character', -val.length);
3554 end = range.text.length;
3557 // caret positioning not supported
3558 start = end = (el.value || '').length;
3560 txt = (el.value || '');
3564 text: txt.substring(start, end),
3565 replace: function (str) {
3566 return txt.substring(0, start) + str + txt.substring(end, txt.length);