Bug 17178: Add virtual keyboard to advanced cataloguing editor
[koha.git] / koha-tmpl / intranet-tmpl / lib / keyboard / js / jquery.keyboard.js
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
5
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.
9
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
13
14 Requires:
15         jQuery v1.4.3+
16         Caret plugin (included)
17 Optional:
18         jQuery UI (position utility only) & CSS theme
19         jQuery mousewheel
20
21 Setup/Usage:
22         Please refer to https://github.com/Mottie/Keyboard/wiki
23
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 -----------------------------------------
29 */
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'));
37         } else {
38                 factory(jQuery);
39         }
40 }(function ($) {
41         'use strict';
42         var $keyboard = $.keyboard = function (el, options) {
43         var o, base = this;
44
45         base.version = '1.29.1';
46
47         // Access to jQuery and DOM versions of element
48         base.$el = $(el);
49         base.el = el;
50
51         // Add a reverse reference to the DOM object
52         base.$el.data('keyboard', base);
53
54         base.init = function () {
55                 base.initialized = false;
56                 var k, position, tmp,
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;
64                 }
65                 base.options = o = $.extend(true, {}, $keyboard.defaultOptions, options);
66                 if (position) {
67                         o.position = position;
68                         options.position = position;
69                 }
70
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'];
82
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
91                 base.isOpen = false;
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 &&
100                         !base.isInput &&
101                         base.el.isContentEditable;
102
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 = [
107                         k.capsLock,
108                         k.pageUp,
109                         k.pageDown,
110                         k.end,
111                         k.home,
112                         k.left,
113                         k.up,
114                         k.right,
115                         k.down,
116                         k.insert,
117                         k.delete
118                 ];
119                 base.$keyboard = [];
120                 // keyboard enabled; set to false on destroy
121                 base.enabled = true;
122
123                 base.checkCaret = (o.lockInput || $keyboard.checkCaretSupport());
124
125                 // disable problematic usePreview for contenteditable
126                 if (base.isContentEditable) {
127                         o.usePreview = false;
128                 }
129
130                 base.last = {
131                         start: 0,
132                         end: 0,
133                         key: '',
134                         val: '',
135                         preVal: '',
136                         layout: '',
137                         virtual: true,
138                         keyset: [false, false, false], // [shift, alt, meta]
139                         wheel_$Keys: [],
140                         wheelIndex: 0,
141                         wheelLayers: []
142                 };
143                 // used when building the keyboard - [keyset element, row, index]
144                 base.temp = ['', 0, 0];
145
146                 // Callbacks
147                 $.each([
148                         kbevents.kbInit,
149                         kbevents.kbBeforeVisible,
150                         kbevents.kbVisible,
151                         kbevents.kbHidden,
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]);
160                         }
161                 });
162
163                 // Close with esc key & clicking outside
164                 if (o.alwaysOpen) {
165                         o.stayOpen = true;
166                 }
167
168                 tmp = $(document);
169                 if (base.el.ownerDocument !== document) {
170                         tmp = tmp.add(base.el.ownerDocument);
171                 }
172
173                 var bindings = 'keyup checkkeyboard mousedown touchstart ';
174                 if (o.closeByClickEvent) {
175                         bindings += 'click ';
176                 }
177                 // debounce bindings... see #542
178                 tmp.bind(bindings.split(' ').join(base.namespace + ' '), function(e) {
179                         clearTimeout(base.timer3);
180                         base.timer3 = setTimeout(function() {
181                                 base.checkClose(e);
182                         }, 1);
183                 });
184
185                 // Display keyboard on focus
186                 base.$el
187                         .addClass(kbcss.input + ' ' + o.css.input)
188                         .attr({
189                                 'aria-haspopup': 'true',
190                                 'role': 'textbox'
191                         });
192
193                 // set lockInput if the element is readonly; or make the element readonly if lockInput is set
194                 if (o.lockInput || base.el.readOnly) {
195                         o.lockInput = true;
196                         base.$el
197                                 .addClass(kbcss.locked)
198                                 .attr({
199                                         'readonly': 'readonly'
200                                 });
201                 }
202                 // add disabled/readonly class - dynamically updated on reveal
203                 if (base.isUnavailable()) {
204                         base.$el.addClass(kbcss.noKeyboard);
205                 }
206                 if (o.openOn) {
207                         base.bindFocus();
208                 }
209
210                 // Add placeholder if not supported by the browser
211                 if (
212                         !base.watermark &&
213                         base.getValue(base.$el) === '' &&
214                         base.inPlaceholder !== '' &&
215                         base.$el.attr('placeholder') !== ''
216                 ) {
217                         // css watermark style (darker text)
218                         base.$el.addClass(kbcss.placeholder);
219                         base.setValue(base.inPlaceholder, base.$el);
220                 }
221
222                 base.$el.trigger(kbevents.kbInit, [base, base.el]);
223
224                 // initialized with keyboard open
225                 if (o.alwaysOpen) {
226                         base.reveal();
227                 }
228                 base.initialized = true;
229         };
230
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
238                 base.$keyboard
239                         .toggleClass($keyboard.css.keyDisabled, locked)
240                         .find('.' + $keyboard.css.keyButton)
241                         .not($toggle)
242                         .attr('aria-disabled', locked)
243                         .each(function() {
244                                 this.disabled = locked;
245                         });
246                 $toggle.toggleClass($keyboard.css.keyDisabled, locked);
247                 // stop auto typing
248                 if (locked && base.typing_options) {
249                         base.typing_options.text = '';
250                 }
251                 // allow chaining
252                 return base;
253         };
254
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);
263                 }
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);
268
269                 base.$el.addClass(kbcss.isCurrent);
270                 base.$keyboard.addClass(kbcss.hasFocus);
271                 base.isCurrent(true);
272                 base.isOpen = true;
273         };
274
275         base.isUnavailable = function() {
276                 return (
277                         base.$el.is(':disabled') || (
278                                 !base.options.activeOnReadonly &&
279                                 base.$el.attr('readonly') &&
280                                 !base.$el.hasClass($keyboard.css.locked)
281                         )
282                 );
283         };
284
285         base.isCurrent = function (set) {
286                 var cur = $keyboard.currentKeyboard || false;
287                 if (set) {
288                         cur = $keyboard.currentKeyboard = base.el;
289                 } else if (set === false && cur === base.el) {
290                         cur = $keyboard.currentKeyboard = '';
291                 }
292                 return cur === base.el;
293         };
294
295         base.hasKeyboard = function () {
296                 return base.$keyboard && base.$keyboard.length > 0;
297         };
298
299         base.isVisible = function () {
300                 return base.hasKeyboard() ? base.$keyboard.is(':visible') : false;
301         };
302
303         base.setFocus = function () {
304                 var $el = base.$preview || base.$el;
305                 if (!o.noFocus) {
306                         $el.focus();
307                 }
308                 if (base.isContentEditable) {
309                         $keyboard.setEditableCaret($el, base.last.start, base.last.end);
310                 } else {
311                         $keyboard.caret($el, base.last);
312                 }
313         };
314
315         base.focusOn = function () {
316                 if (!base && base.el.active) {
317                         // keyboard was destroyed
318                         return;
319                 }
320                 if (!base.isVisible()) {
321                         clearTimeout(base.timer);
322                         base.reveal();
323                 } else {
324                         // keyboard already open, make it the current keyboard
325                         base.setCurrent();
326                 }
327         };
328
329         // add redraw method to make API more clear
330         base.redraw = function (layout) {
331                 if (layout) {
332                         // allow updating the layout by calling redraw
333                         base.options.layout = layout;
334                 }
335                 // update keyboard after a layout change
336                 if (base.$keyboard.length) {
337
338                         base.last.preVal = '' + base.last.val;
339                         base.saveLastChange();
340                         base.setValue(base.last.val, base.$el);
341
342                         base.removeKeyboard();
343                         base.shiftActive = base.altActive = base.metaActive = false;
344                 }
345                 base.isOpen = o.alwaysOpen;
346                 base.reveal(true);
347                 return base;
348         };
349
350         base.reveal = function (redraw) {
351                 var temp,
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);
361                         }
362                 });
363
364                 // Don't open if disabled
365                 if (base.isUnavailable()) {
366                         return;
367                 }
368                 base.$el.removeClass(kbcss.noKeyboard);
369
370                 // Unbind focus to prevent recursion - openOn may be empty if keyboard is opened externally
371                 if (o.openOn) {
372                         base.$el.unbind($.trim((o.openOn + ' ').split(/\s+/).join(base.namespace + ' ')));
373                 }
374
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]))) {
378                         base.startup();
379                 }
380
381                 // clear watermark
382                 if (!base.watermark && base.getValue() === base.inPlaceholder) {
383                         base.$el.removeClass(kbcss.placeholder);
384                         base.setValue('', base.$el);
385                 }
386                 // save starting content, in case we cancel
387                 base.originalContent = base.isContentEditable ?
388                         base.$el.html() :
389                         base.getValue(base.$el);
390                 if (base.el !== base.preview && !base.isContentEditable) {
391                         base.setValue(base.originalContent);
392                 }
393
394                 // disable/enable accept button
395                 if (o.acceptValid && o.checkValidOnInit) {
396                         base.checkValid();
397                 }
398
399                 if (o.resetDefault) {
400                         base.shiftActive = base.altActive = base.metaActive = false;
401                 }
402                 base.showSet();
403
404                 // beforeVisible event
405                 if (!base.isVisible()) {
406                         base.$el.trigger($keyboard.events.kbBeforeVisible, [base, base.el]);
407                 }
408                 if (
409                         base.initialized ||
410                         o.initialFocus ||
411                         ( !o.initialFocus && base.$el.hasClass($keyboard.css.initialFocus) )
412                 ) {
413                         base.setCurrent();
414                 }
415                 // update keyboard - enabled or disabled?
416                 base.toggle();
417
418                 // show keyboard
419                 base.$keyboard.show();
420
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();
427                         }
428                         base.$preview.width(base.width);
429                 }
430
431                 base.reposition();
432
433                 base.checkDecimal();
434
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;
440
441                 if (o.caretToEnd) {
442                         temp = base.isContentEditable ? $keyboard.getEditableLength(base.el) : base.originalContent.length;
443                         base.saveCaret(temp, temp);
444                 }
445
446                 // IE caret haxx0rs
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;
451                         }
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;
456                         }
457                 }
458
459                 if (alreadyOpen || redraw) {
460                         // restore caret position (userClosed)
461                         $keyboard.caret(base.$preview, base.last);
462                         base.opening = false;
463                         return base;
464                 }
465
466                 // opening keyboard flag; delay allows switching between keyboards without immediately closing
467                 // the keyboard
468                 base.timer2 = setTimeout(function () {
469                         var undef;
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);
478                         }
479                         if (o.initialFocus || base.$el.hasClass($keyboard.css.initialFocus)) {
480                                 $keyboard.caret(base.$preview, base.last);
481                         }
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
488                                         base.saveCaret();
489                                 }
490                         }, 200);
491                 }, 10);
492                 // return base to allow chaining in typing extension
493                 return base;
494         };
495
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;
502
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];
509
510                 // set keyboard language
511                 o.display = $.extend(true, {},
512                         kblang.en.display,
513                         kblang[lang] && kblang[lang].display || {},
514                         base.settings.display
515                 );
516                 o.combos = $.extend(true, {},
517                         kblang.en.combos,
518                         kblang[lang] && kblang[lang].combos || {},
519                         base.settings.combos
520                 );
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;
524
525                 // save default regex (in case loading another layout changes it)
526                 if (kblang[lang] && kblang[lang].comboRegex) {
527                         base.regex = kblang[lang].comboRegex;
528                 }
529                 // determine if US '.' or European ',' system being used
530                 base.decimal = /^\./.test(o.display.dec);
531                 base.$el
532                         .toggleClass('rtl', o.rtl)
533                         .css('direction', o.rtl ? 'rtl' : '');
534         };
535
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)) {
540                         base.makePreview();
541                 }
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();
546                         }
547                         base.layout = o.layout === 'custom' ? o.layoutHash : o.layout;
548                         base.last.layout = base.layout;
549
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);
557                                 }
558                         }
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);
564                         }
565
566                         base.makePreview();
567                 }
568
569                 // Add layout and laguage data-attibutes
570                 base.$keyboard
571                         .attr('data-' + kbcss.keyboard + '-layout', o.layout)
572                         .attr('data-' + kbcss.keyboard + '-language', base.language);
573
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);
578                 }
579
580                 base.bindKeyboard();
581
582                 base.$keyboard.appendTo(o.appendLocally ? base.$el.parent() : o.appendTo || 'body');
583
584                 base.bindKeys();
585
586                 // reposition keyboard on window resize
587                 if (o.reposition && $.ui && $.ui.position && o.appendTo === 'body') {
588                         $(window).bind('resize' + base.namespace, function () {
589                                 base.reposition();
590                         });
591                 }
592
593         };
594
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) {
600                         base.position.of =
601                                 // get single target position
602                                 base.position.of ||
603                                 // OR target stored in element data (multiple targets)
604                                 base.$el.data('keyboardPosition') ||
605                                 // OR default @ element
606                                 base.$el;
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);
611                         }
612                 }
613                 // make chainable
614                 return base;
615         };
616
617         base.makePreview = function () {
618                 if (o.usePreview) {
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];
628
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';
632                         }
633
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);
642                                 }
643                         }
644                         // build preview container and append preview display
645                         $('<div />')
646                                 .addClass(kbcss.wrapper)
647                                 .append(base.$preview)
648                                 .prependTo(base.$keyboard);
649                 } else {
650                         base.$preview = base.$el;
651                         base.preview = base.el;
652                 }
653         };
654
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
661                 if (wasSetCaret) {
662                         base.saveCaret(result.start, result.end);
663                         return base;
664                 }
665                 // return caret position if using .caret()
666                 return result;
667         };
668
669         base.saveCaret = function (start, end, $el) {
670                 if (base.isCurrent()) {
671                         var p;
672                         if (typeof start === 'undefined') {
673                                 // grab & save current caret position
674                                 p = $keyboard.caret($el || base.$preview);
675                         } else {
676                                 p = $keyboard.caret($el || base.$preview, start, end);
677                         }
678                         base.last.start = typeof start === 'undefined' ? p.start : start;
679                         base.last.end = typeof end === 'undefined' ? p.end : end;
680                 }
681         };
682
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);
687                 }
688         };
689
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) {
694
695                         var scrollWidth, clientWidth, adjustment, direction,
696                                 value = base.last.val.substring(0, Math.max(base.last.start, base.last.end));
697
698                         if (!base.$previewCopy) {
699                                 // clone preview
700                                 base.$previewCopy = base.$preview.clone()
701                                         .removeAttr('id') // fixes #334
702                                         .css({
703                                                 position: 'absolute',
704                                                 left: 0,
705                                                 zIndex: -10,
706                                                 visibility: 'hidden'
707                                         })
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',
715                                                 'width': 0
716                                         });
717                                 }
718                                 if (o.usePreview) {
719                                         // add clone inside of preview wrapper
720                                         base.$preview.after(base.$previewCopy);
721                                 } else {
722                                         // just slap that thing in there somewhere
723                                         base.$keyboard.prepend(base.$previewCopy);
724                                 }
725                         }
726
727                         if (base.isTextArea) {
728                                 // need the textarea scrollHeight, so set the clone textarea height to be the line height
729                                 base.$previewCopy
730                                         .height(base.lineHeight)
731                                         .val(value);
732                                 // set scrollTop for Textarea
733                                 base.preview.scrollTop = base.lineHeight *
734                                         (Math.floor(base.$previewCopy[0].scrollHeight / base.lineHeight) - 1);
735                         } else {
736                                 // add non-breaking spaces
737                                 base.$previewCopy.val(value.replace(/\s/g, '\xa0'));
738
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;
742
743                                 // set initial state as moving right
744                                 if (typeof base.last.scrollWidth === 'undefined') {
745                                         base.last.scrollWidth = scrollWidth;
746                                         base.last.direction = true;
747                                 }
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;
753
754                                 // set scrollLeft for inputs; try to mimic the inherit caret positioning + scrolling:
755                                 // hug right while scrolling right...
756                                 if (direction) {
757                                         if (scrollWidth < clientWidth) {
758                                                 base.preview.scrollLeft = 0;
759                                         } else {
760                                                 base.preview.scrollLeft = scrollWidth - clientWidth;
761                                         }
762                                 } else {
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;
768                                         } else {
769                                                 base.preview.scrollLeft = 0;
770                                         }
771                                 }
772
773                                 base.last.scrollWidth = scrollWidth;
774                                 base.last.direction = direction;
775                         }
776                 }
777         };
778
779         base.bindFocus = function () {
780                 if (o.openOn) {
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 () {
785                                         base.focusOn();
786                                 });
787                                 // remove focus from element (needed for IE since blur doesn't seem to work)
788                                 if ($(':focus')[0] === base.el) {
789                                         base.$el.blur();
790                                 }
791                         }
792                 }
793         };
794
795         base.bindKeyboard = function () {
796                 var evt,
797                         keyCodes = $keyboard.keyCodes,
798                         layout = $keyboard.builtLayouts[base.layout],
799                         namespace = base.namespace + 'keybindings';
800                 base.$preview
801                         .unbind(base.namespace)
802                         .bind('click' + namespace + ' touchstart' + namespace, function () {
803                                 if (o.alwaysOpen && !base.isCurrent()) {
804                                         base.reveal();
805                                 }
806                                 // update last caret position after user click, use at least 150ms or it doesn't work in IE
807                                 base.timer2 = setTimeout(function () {
808                                         if (base){
809                                                 base.saveCaret();
810                                         }
811                                 }, 150);
812
813                         })
814                         .bind('keypress' + namespace, function (e) {
815                                 if (o.lockInput) {
816                                         return false;
817                                 }
818                                 if (!base.isCurrent()) {
819                                         return;
820                                 }
821
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
830                                         return;
831                                 }
832                                 base.last.virtual = false;
833                                 base.last.event = e;
834                                 base.last.$key = []; // not a virtual keyboard key
835                                 if (base.checkCaret) {
836                                         base.saveCaret();
837                                 }
838
839                                 // update capsLock
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;
845                                                 base.showSet();
846                                         }
847                                 }
848
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)) {
855                                                 return;
856                                         }
857                                         // quick key check
858                                         if ($.inArray(str, layout.acceptedKeys) === -1) {
859                                                 e.preventDefault();
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]);
864                                         }
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
870                                         return;
871                                 }
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);
879                                         e.preventDefault();
880                                 }
881                                 if (typeof o.beforeInsert === 'function') {
882                                         base.insertText(base.last.key);
883                                         e.preventDefault();
884                                 }
885                                 base.checkMaxLength();
886
887                         })
888                         .bind('keyup' + namespace, function (e) {
889                                 if (!base.isCurrent()) { return; }
890                                 base.last.virtual = false;
891                                 switch (e.which) {
892                                         // Insert tab key
893                                 case keyCodes.tab:
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);
901                                                 base.tab = false;
902                                                 if (!notSwitching) {
903                                                         return false;
904                                                 }
905                                         } else {
906                                                 e.preventDefault();
907                                         }
908                                         break;
909
910                                         // Escape will hide the keyboard
911                                 case keyCodes.escape:
912                                         if (!o.ignoreEsc) {
913                                                 base.close(o.autoAccept && o.autoAcceptOnEsc ? 'true' : false);
914                                         }
915                                         return false;
916                                 }
917
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()) {
923                                                 base.checkCombos();
924                                         }
925                                 }, 100);
926
927                                 base.checkMaxLength();
928
929                                 base.last.preVal = '' + base.last.val;
930                                 base.saveLastChange();
931
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]);
938
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);
944                                         return false;
945                                 }
946                                 if (o.acceptValid && o.autoAcceptOnValid) {
947                                         if (
948                                                 typeof o.validate === 'function' &&
949                                                 o.validate(base, base.getValue(base.$preview))
950                                         ) {
951                                                 base.$preview.blur();
952                                                 base.accept();
953                                         }
954                                 }
955                         })
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()) {
960                                         base.reveal();
961                                 }
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
965                                         base.tab = true;
966                                         return false;
967                                 }
968                                 if (o.lockInput || e.timeStamp === base.last.timeStamp) {
969                                         return !o.lockInput;
970                                 }
971
972                                 base.last.timeStamp = e.timeStamp; // fixes #659
973                                 base.last.virtual = false;
974                                 switch (e.which) {
975
976                                 case keyCodes.backSpace:
977                                         $keyboard.keyaction.bksp(base, null, e);
978                                         e.preventDefault();
979                                         break;
980
981                                 case keyCodes.enter:
982                                         $keyboard.keyaction.enter(base, null, e);
983                                         break;
984
985                                         // Show capsLock
986                                 case keyCodes.capsLock:
987                                         base.shiftActive = base.capsLock = !base.capsLock;
988                                         base.showSet();
989                                         break;
990
991                                 case keyCodes.V:
992                                         // prevent ctrl-v/cmd-v
993                                         if (e.ctrlKey || e.metaKey) {
994                                                 if (o.preventPaste) {
995                                                         e.preventDefault();
996                                                         return;
997                                                 }
998                                                 base.checkCombos(); // check pasted content
999                                         }
1000                                         break;
1001                                 }
1002                         })
1003                         .bind('mouseup touchend '.split(' ').join(namespace + ' '), function () {
1004                                 base.last.virtual = true;
1005                                 base.saveCaret();
1006                         });
1007
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()) {
1012                                 base.reveal();
1013                                 $(base.el.ownerDocument).trigger('checkkeyboard' + base.namespace);
1014                         }
1015                         base.setFocus();
1016                 });
1017
1018                 // If preventing paste, block context menu (right click)
1019                 if (o.preventPaste) {
1020                         base.$preview.bind('contextmenu' + base.namespace, function (e) {
1021                                 e.preventDefault();
1022                         });
1023                         base.$el.bind('contextmenu' + base.namespace, function (e) {
1024                                 e.preventDefault();
1025                         });
1026                 }
1027
1028         };
1029
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);
1037                         };
1038                 if ($.fn.on) {
1039                         // jQuery v1.7+
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);
1044                 }
1045                 return base;
1046         };
1047
1048         base.unbindButton = function(namespace) {
1049                 if ($.fn.off) {
1050                         // jQuery v1.7+
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);
1055                 }
1056                 return base;
1057         };
1058
1059         base.bindKeys = function () {
1060                 var kbcss = $keyboard.css;
1061                 base
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()) {
1067                                         base.reveal();
1068                                         base.setFocus();
1069                                 }
1070                                 if (!base.isCurrent() || this.disabled) {
1071                                         return;
1072                                 }
1073                                 var $keys, txt,
1074                                         last = base.last,
1075                                         $this = $(this),
1076                                         type = e.type;
1077
1078                                 if (o.useWheel && base.wheel) {
1079                                         $keys = base.getLayers($this);
1080                                         txt = ($keys.length ? $keys.map(function () {
1081                                                         return $(this).attr('data-value') || '';
1082                                                 })
1083                                                 .get() : '') || [$this.text()];
1084                                         last.wheel_$Keys = $keys;
1085                                         last.wheelLayers = txt;
1086                                         last.wheelIndex = $.inArray($this.attr('data-value'), txt);
1087                                 }
1088
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') ?
1096                                                                 o.wheelMessage : t;
1097                                                 });
1098                                         }
1099                                 }
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 = [];
1107                                                 $this
1108                                                         .attr('title', function (i, t) {
1109                                                                 return (t === o.wheelMessage) ? '' : t;
1110                                                         })
1111                                                         .html($this.attr('data-html')); // restore original button text
1112                                         }
1113                                 }
1114                         })
1115                         // keyBinding = 'mousedown touchstart' by default
1116                         .bindButton(o.keyBinding.split(' ').join(base.namespace + ' ') + base.namespace + ' ' +
1117                                 $keyboard.events.kbRepeater, function (e) {
1118                                 e.preventDefault();
1119                                 // prevent errors when external triggers attempt to 'type' - see issue #158
1120                                 if (!base.$keyboard.is(':visible') || this.disabled) {
1121                                         return false;
1122                                 }
1123                                 var action,
1124                                         last = base.last,
1125                                         $key = $(this),
1126                                         // prevent mousedown & touchstart from both firing events at the same time - see #184
1127                                         timer = new Date().getTime();
1128
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;
1133                                 }
1134                                 action = $key.attr('data-action');
1135                                 if (timer - (last.eventTime || 0) < o.preventDoubleEventTime) {
1136                                         return;
1137                                 }
1138                                 last.eventTime = timer;
1139                                 last.event = e;
1140                                 last.virtual = true;
1141                                 last.$key = $key;
1142                                 last.key = $key.attr('data-value');
1143                                 last.keyPress = '';
1144                                 // Start caret in IE when not focused (happens with each virtual keyboard button click
1145                                 base.setFocus();
1146                                 if (/^meta/.test(action)) {
1147                                         action = 'meta';
1148                                 }
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) {
1155                                                 return false;
1156                                         }
1157                                         action = null; // prevent inserting action name
1158                                 }
1159                                 // stop processing if keyboard closed and keyaction did not return false - see #536
1160                                 if (!base.hasKeyboard()) {
1161                                         return false;
1162                                 }
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'));
1169                                         }
1170                                 }
1171                                 // set caret if caret moved by action function; also, attempt to fix issue #131
1172                                 $keyboard.caret(base.$preview, last);
1173                                 base.checkCombos();
1174                                 e = $.extend({}, e, $.Event($keyboard.events.kbChange));
1175                                 e.target = base.el;
1176                                 e.action = last.key;
1177                                 base.$el.trigger(e, [base, base.el]);
1178                                 last.preVal = '' + last.val;
1179                                 base.saveLastChange();
1180
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
1185                                         return false;
1186                                 }
1187
1188                         })
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;
1194                                 var offset,
1195                                         $this = $(this);
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) {
1205                                                 return true;
1206                                         }
1207                                 } else if (/(mouseleave|touchend|touchcancel)/i.test(e.type)) {
1208                                         $this.removeClass(o.css.buttonHover); // needed for touch devices
1209                                 } else {
1210                                         if (!o.noFocus && base.isCurrent() && base.isVisible()) {
1211                                                 base.$preview.focus();
1212                                         }
1213                                         if (base.checkCaret) {
1214                                                 $keyboard.caret(base.$preview, base.last);
1215                                         }
1216                                 }
1217                                 base.mouseRepeat = [false, ''];
1218                                 clearTimeout(base.repeater); // make sure key repeat stops!
1219                                 if (o.acceptValid && o.autoAcceptOnValid) {
1220                                         if (
1221                                                 typeof o.validate === 'function' &&
1222                                                 o.validate(base, base.getValue())
1223                                         ) {
1224                                                 base.$preview.blur();
1225                                                 base.accept();
1226                                         }
1227                                 }
1228                                 return false;
1229                         })
1230                         // prevent form submits when keyboard is bound locally - issue #64
1231                         .bindButton('click' + base.namespace, function () {
1232                                 return false;
1233                         })
1234                         // Allow mousewheel to scroll through other keysets of the same (non-action) key
1235                         .bindButton('mousewheel' + base.namespace, base.throttleEvent(function (e, delta) {
1236                                 var $btn = $(this);
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) {
1239                                         return;
1240                                 }
1241                                 if (o.useWheel && base.wheel) {
1242                                         // deltaY used by newer versions of mousewheel plugin
1243                                         delta = delta || e.deltaY;
1244                                         var n,
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) {
1249                                                         n = 0;
1250                                                 }
1251                                                 if (n < 0) {
1252                                                         n = txt.length - 1;
1253                                                 }
1254                                         } else {
1255                                                 n = 0;
1256                                         }
1257                                         base.last.wheelIndex = n;
1258                                         $btn.html(txt[n]);
1259                                         return false;
1260                                 }
1261                         }, 30))
1262                         .bindButton('mousedown touchstart '.split(' ').join(base.namespace + 'kb '), function () {
1263                                 var $btn = $(this);
1264                                 // no mouse repeat for action keys (shift, ctrl, alt, meta, etc)
1265                                 if (
1266                                         !$btn || (
1267                                                 $btn.hasClass(kbcss.keyAction) &&
1268                                                 // mouse repeated action key exceptions
1269                                                 !$btn.is('.' + kbcss.keyPrefix + ('tab bksp space enter'.split(' ').join(',.' + kbcss.keyPrefix)))
1270                                         )
1271                                 ) {
1272                                         return;
1273                                 }
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);
1281                                                 }
1282                                         }, o.repeatDelay);
1283                                 }
1284                                 return false;
1285                         });
1286         };
1287
1288         // No call on tailing event
1289         base.throttleEvent = function(cb, time) {
1290                 var interm;
1291                 return function() {
1292                         if (!interm) {
1293                                 cb.apply(this, arguments);
1294                                 interm = true;
1295                                 setTimeout(function() {
1296                                         interm = false;
1297                                 }, time);
1298                         }
1299                 };
1300         };
1301
1302         base.execCommand = function(cmd, str) {
1303                 base.el.ownerDocument.execCommand(cmd, false, str);
1304                 base.el.normalize();
1305                 if (o.reposition) {
1306                         base.reposition();
1307                 }
1308         };
1309
1310         base.getValue = function ($el) {
1311                 $el = $el || base.$preview;
1312                 return $el[base.isContentEditable ? 'text' : 'val']();
1313         };
1314
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);
1320                                 base.saveCaret();
1321                         }
1322                 } else {
1323                         $el.val(txt);
1324                 }
1325                 return base;
1326         };
1327
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);
1333                 }
1334                 if (typeof txt === 'undefined' || txt === false) {
1335                         base.last.key = '';
1336                         return base;
1337                 }
1338                 if (base.isContentEditable) {
1339                         return base.insertContentEditable(txt);
1340                 }
1341                 var t,
1342                         bksp = false,
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
1348
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;
1354                 }
1355                 if (pos.start > len) {
1356                         pos.end = pos.start = len;
1357                 }
1358
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') {
1362                                 pos.start += 1;
1363                                 pos.end += 1;
1364                         }
1365                 }
1366
1367                 t = pos.start;
1368                 if (txt === '{d}') {
1369                         txt = '';
1370                         pos.end += 1;
1371                 }
1372
1373                 if (isBksp) {
1374                         txt = '';
1375                         bksp = isBksp && t === pos.end && t > 0;
1376                 }
1377                 val = val.substring(0, t - (bksp ? 1 : 0)) + txt + val.substring(pos.end);
1378                 t += bksp ? -1 : txt.length;
1379
1380                 base.setValue(val);
1381                 base.saveCaret(t, t); // save caret in case of bksp
1382                 base.setScroll();
1383                 // see #506.. allow chaining of insertText
1384                 return base;
1385         };
1386
1387         base.insertContentEditable = function (txt) {
1388                 base.$preview.focus();
1389                 base.execCommand('insertText', txt);
1390                 base.saveCaret();
1391                 return base;
1392         };
1393
1394         // check max length
1395         base.checkMaxLength = function () {
1396                 if (!base.$preview) { return; }
1397                 var start, caret,
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);
1403
1404                         // prevent inserting new characters when maxed #289
1405                         if (!o.maxInsert) {
1406                                 val = base.last.val;
1407                                 caret = start - 1; // move caret back one
1408                         }
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);
1412                 }
1413                 if (base.$decBtn.length) {
1414                         base.checkDecimal();
1415                 }
1416                 // allow chaining
1417                 return base;
1418         };
1419
1420         // mousedown repeater
1421         base.repeatKey = function (key) {
1422                 key.trigger($keyboard.events.kbRepeater);
1423                 if (base.mouseRepeat[0]) {
1424                         base.repeater = setTimeout(function () {
1425                                 if (base){
1426                                         base.repeatKey(key);
1427                                 }
1428                         }, base.repeatTime);
1429                 }
1430         };
1431
1432         base.getKeySet = function () {
1433                 var sets = [];
1434                 if (base.altActive) {
1435                         sets.push('alt');
1436                 }
1437                 if (base.shiftActive) {
1438                         sets.push('shift');
1439                 }
1440                 if (base.metaActive) {
1441                         // base.metaActive contains the string name of the
1442                         // current meta keyset
1443                         sets.push(base.metaActive);
1444                 }
1445                 return sets.length ? sets.join('+') : 'normal';
1446         };
1447
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]);
1458                         } else {
1459                                 base.metaActive = false;
1460                                 base.showSet();
1461                         }
1462                 } else {
1463                         base.showSet(str);
1464                 }
1465                 // allow chaining
1466                 return base;
1467         };
1468
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,
1475                         key = '',
1476                         toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0);
1477                 if (!base.shiftActive) {
1478                         base.capsLock = false;
1479                 }
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', '');
1485                         }
1486                         if (base.altActive) {
1487                                 name = (name || '').replace('-alt', '');
1488                         }
1489                         // the name attribute contains the meta set name 'meta99'
1490                         key = (/^meta/i.test(name)) ? name : '';
1491                         // save active meta keyset name
1492                         if (key === '') {
1493                                 key = (base.metaActive === true) ? '' : base.metaActive;
1494                         } else {
1495                                 base.metaActive = key;
1496                         }
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;
1502                         }
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;
1506                 }
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];
1514                         return;
1515                 }
1516                 base.$keyboard
1517                         .find(prefix + 'alt,' + prefix + 'shift,.' + kbcss.keyAction + '[class*=meta]')
1518                         .removeClass(active)
1519                         .end()
1520                         .find(prefix + 'alt')
1521                         .toggleClass(active, base.altActive)
1522                         .end()
1523                         .find(prefix + 'shift')
1524                         .toggleClass(active, base.shiftActive)
1525                         .end()
1526                         .find(prefix + 'lock')
1527                         .toggleClass(active, base.capsLock)
1528                         .end()
1529                         .find('.' + kbcss.keySet)
1530                         .hide()
1531                         .end()
1532                         .find('.' + (kbcss.keyAction + prefix + key).replace('--', '-'))
1533                         .addClass(active);
1534
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);
1542                 }
1543                 base.last.keyset = [base.shiftActive, base.altActive, base.metaActive];
1544                 base.$el.trigger($keyboard.events.kbKeysetChange, [base, base.el]);
1545                 if (o.reposition) {
1546                         base.reposition();
1547                 }
1548         };
1549
1550         // check for key combos (dead keys)
1551         base.checkCombos = function () {
1552                 // return val for close function
1553                 if ( !(
1554                         base.isVisible() || (
1555                                 base.hasKeyboard() &&
1556                                 base.$keyboard.hasClass( $keyboard.css.hasFocus )
1557                         )
1558                 ) ) {
1559                         return base.getValue(base.$preview || base.$el);
1560                 }
1561                 var r, t, t2, repl,
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
1569                         len = max;
1570                 // return if val is empty; fixes #352
1571                 if (val === '') {
1572                         // check valid on empty string - see #429
1573                         if (o.acceptValid) {
1574                                 base.checkValid();
1575                         }
1576                         return val;
1577                 }
1578
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;
1584                 }
1585                 if (pos.start > len) {
1586                         pos.end = pos.start = len;
1587                 }
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') {
1590                         pos.start += 1;
1591                         pos.end += 1;
1592                 }
1593
1594                 if (o.useCombos) {
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;
1602                                 });
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);
1609                                 // do combo replace
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;
1614                                         });
1615                                 };
1616                                 t2 = repl(t.text);
1617                                 // add combo back
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);
1622                                         } else {
1623                                                 base.setValue(t.replaceStr(t2));
1624                                         }
1625                                 }
1626                                 val = base.getValue();
1627                         }
1628                 }
1629
1630                 // check input restrictions - in case content was pasted
1631                 if (o.restrictInput && val !== '') {
1632                         t = layout.acceptedKeys.length;
1633
1634                         r = layout.acceptedKeysRegex;
1635                         if (!r) {
1636                                 t2 = $.map(layout.acceptedKeys, function (v) {
1637                                         // escape any special characters
1638                                         return v.replace(base.escapeRegex, '\\$&');
1639                                 });
1640                                 if (base.alwaysAllowed.indexOf($keyboard.keyCodes.enter) > -1) {
1641                                         t2.push('\\n'); // Fixes #686
1642                                 }
1643                                 r = layout.acceptedKeysRegex = new RegExp('(' + t2.join('|') + ')', 'g');
1644                         }
1645                         // only save matching keys
1646                         t2 = val.match(r);
1647                         if (t2) {
1648                                 val = t2.join('');
1649                         } else {
1650                                 // no valid characters
1651                                 val = '';
1652                                 len = 0;
1653                         }
1654                 }
1655
1656                 // save changes, then reposition caret
1657                 pos.start += max - len;
1658                 pos.end += max - len;
1659
1660                 base.setValue(val);
1661                 base.saveCaret(pos.start, pos.end);
1662                 // set scroll to keep caret in view
1663                 base.setScroll();
1664                 base.checkMaxLength();
1665
1666                 if (o.acceptValid) {
1667                         base.checkValid();
1668                 }
1669                 return val; // return text, used for keyboard closing section
1670         };
1671
1672         // Toggle accept button classes, if validating
1673         base.checkValid = function () {
1674                 var kbcss = $keyboard.css,
1675                         $accept = base.$keyboard.find('.' + kbcss.keyPrefix + 'accept'),
1676                         valid = true;
1677                 if (typeof o.validate === 'function') {
1678                         valid = o.validate(base, base.getValue(), false);
1679                 }
1680                 // toggle accept button classes; defined in the css
1681                 $accept
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'] + ')');
1686         };
1687
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))) {
1693                         base.$decBtn
1694                                 .attr({
1695                                         'disabled': 'disabled',
1696                                         'aria-disabled': 'true'
1697                                 })
1698                                 .removeClass(o.css.buttonHover)
1699                                 .addClass(o.css.buttonDisabled);
1700                 } else {
1701                         base.$decBtn
1702                                 .removeAttr('disabled')
1703                                 .attr({
1704                                         'aria-disabled': 'false'
1705                                 })
1706                                 .addClass(o.css.buttonDefault)
1707                                 .removeClass(o.css.buttonDisabled);
1708                 }
1709         };
1710
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 () {
1718                         return $(this)
1719                                 .find('.' + kbcss.keyText)
1720                                 .text() !== '';
1721                 })
1722                 .add($el);
1723         };
1724
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);
1731                 } else {
1732                         // base.$keyboard may be an empty array - see #275 (apod42)
1733                         if (base.$keyboard.length) {
1734                                 base.$keyboard.hide();
1735                         }
1736                         var kb,
1737                                 stopped = false,
1738                                 all = $('button, input, select, textarea, a, [contenteditable]')
1739                                         .filter(':visible')
1740                                         .not(':disabled'),
1741                                 indx = all.index(base.$el) + (goToNext ? 1 : -1);
1742                         if (base.$keyboard.length) {
1743                                 base.$keyboard.show();
1744                         }
1745                         if (indx > all.length - 1) {
1746                                 stopped = o.stopAtEnd;
1747                                 indx = 0; // go to first input
1748                         }
1749                         if (indx < 0) {
1750                                 stopped = o.stopAtEnd;
1751                                 indx = all.length - 1; // stop or go to last
1752                         }
1753                         if (!stopped) {
1754                                 isAccepted = base.close(isAccepted);
1755                                 if (!isAccepted) {
1756                                         return;
1757                                 }
1758                                 kb = all.eq(indx).data('keyboard');
1759                                 if (kb && kb.options.openOn.length) {
1760                                         kb.focusOn();
1761                                 } else {
1762                                         all.eq(indx).focus();
1763                                 }
1764                         }
1765                 }
1766                 return false;
1767         };
1768
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;
1780                                 accepted = false;
1781                                 if (o.cancelClose) {
1782                                         return;
1783                                 }
1784                         }
1785                         base.isCurrent(false);
1786                         base.isOpen = o.alwaysOpen || o.userClosed;
1787                         if (base.isContentEditable && !accepted) {
1788                                 // base.originalContent stores the HTML
1789                                 base.$el.html(val);
1790                         } else {
1791                                 base.setValue(val, base.$el);
1792                         }
1793                         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)]);
1803                         }
1804                         // save caret after updating value (fixes userClosed issue with changing focus)
1805                         $keyboard.caret(base.$preview, base.last);
1806
1807                         base.$el
1808                                 .trigger(((accepted || false) ? kbevents.inputAccepted : kbevents.inputCanceled), [base, base.el])
1809                                 .trigger((o.alwaysOpen) ? kbevents.kbInactive : kbevents.kbHidden, [base, base.el])
1810                                 .blur();
1811
1812                         // base is undefined if keyboard was destroyed - fixes #358
1813                         if (base) {
1814                                 // add close event time
1815                                 base.last.eventTime = new Date().getTime();
1816                                 if (!(o.alwaysOpen || o.userClosed && accepted === 'true') && base.$keyboard.length) {
1817                                         // free up memory
1818                                         base.removeKeyboard();
1819                                         // rebind input focus - delayed to fix IE issue #72
1820                                         base.timer = setTimeout(function () {
1821                                                 if (base) {
1822                                                         base.bindFocus();
1823                                                 }
1824                                         }, 200);
1825                                 }
1826                                 if (!base.watermark && base.el.value === '' && base.inPlaceholder !== '') {
1827                                         base.$el.addClass(kbcss.placeholder);
1828                                         base.setValue(base.inPlaceholder, base.$el);
1829                                 }
1830                         }
1831                 }
1832                 return !!accepted;
1833         };
1834
1835         base.accept = function () {
1836                 return base.close(true);
1837         };
1838
1839         base.checkClose = function (e) {
1840                 if (base.opening) {
1841                         return;
1842                 }
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);
1847                 }
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
1852                         if (
1853                                 kb !== base &&
1854                                 !kb.$el.hasClass(kbcss.isCurrent) &&
1855                                 kb.options.openOn &&
1856                                 e.type === o.openOn
1857                         ) {
1858                                 kb.focusOn();
1859                         }
1860                 } else {
1861                         base.escClose(e, $target);
1862                 }
1863         };
1864
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;
1872                 }
1873         };
1874
1875         base.escClose = function (e, $el) {
1876                 if (!base.isOpen) {
1877                         return;
1878                 }
1879                 if (e && e.type === 'keyup') {
1880                         return (e.which === $keyboard.keyCodes.escape && !o.ignoreEsc) ?
1881                                 base.close(o.autoAccept && o.autoAcceptOnEsc ? 'true' : false) :
1882                                 '';
1883                 }
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);
1889                         }
1890                 });
1891                 if (shouldStayOpen) {
1892                         return;
1893                 }
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)) {
1898                                 return;
1899                         }
1900                         // stop propogation in IE - an input getting focus doesn't open a keyboard if one is already open
1901                         if ($keyboard.allie) {
1902                                 e.preventDefault();
1903                         }
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);
1910                                 }
1911                         } else {
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);
1915                         }
1916                 }
1917         };
1918
1919         // Build default button
1920         base.keyBtn = $('<button />')
1921                 .attr({
1922                         'role': 'button',
1923                         'type': 'button',
1924                         'aria-disabled': 'false',
1925                         'tabindex': '-1'
1926                 })
1927                 .addClass($keyboard.css.keyButton);
1928
1929         // convert key names into a class name
1930         base.processName = function (name) {
1931                 var index, n,
1932                         process = (name || '').replace(/[^a-z0-9-_]/gi, ''),
1933                         len = process.length,
1934                         newName = [];
1935                 if (len > 1 && name === process) {
1936                         // return name if basic text
1937                         return name;
1938                 }
1939                 // return character code sequence
1940                 len = name.length;
1941                 if (len) {
1942                         for (index = 0; index < len; index++) {
1943                                 n = name[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)
1948                                 );
1949                         }
1950                         return newName.join('');
1951                 }
1952                 return name;
1953         };
1954
1955         base.processKeys = function (name) {
1956                 var tmp,
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('<')),
1961                         data = {
1962                                 name: null,
1963                                 map: '',
1964                                 title: ''
1965                         };
1966                 if (htmlIndex > -1 && (colonIndex < 0 || colonIndex > htmlIndex)) {
1967                         // html includes colons; see #701
1968                         data.name = name;
1969                         return data;
1970                 }
1971                 /* map defined keys
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
1977                 Examples:
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)'
1982                 */
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) {
1988                                         data.name = tmp[1];
1989                                         data.map = tmp[2];
1990                                         data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
1991                                 } else {
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);
1997                                         }
1998                                         if (tmp === null) {
1999                                                 // 'x(:):test' => parts = [ 'x(', ')', 'title' ] need to slice 2
2000                                                 data.map = ':';
2001                                                 parts = parts.slice(2);
2002                                         }
2003                                         data.title = parts.length ? parts.join(':') : '';
2004                                 }
2005                         } else {
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] === '') {
2013                                         data.name = ':';
2014                                         parts = parts.slice(1);
2015                                 } else {
2016                                         data.name = tmp[0];
2017                                 }
2018                                 data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
2019                         }
2020                 } else {
2021                         // find key label
2022                         // corner case of '::;' reduced to ':;', split as ['', ';']
2023                         if (name !== '' && parts[0] === '') {
2024                                 data.name = ':';
2025                                 parts = parts.slice(1);
2026                         } else {
2027                                 data.name = parts[0];
2028                         }
2029                         data.title = parts.length > 1 ? parts.slice(1).join(':') : '';
2030                 }
2031                 data.title = $.trim(data.title).replace(/_/g, ' ');
2032                 return data;
2033         };
2034
2035         // Add key function
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,
2042                         data = {},
2043                         txt = base.processKeys(regKey ? keyName : action),
2044                         kbcss = $keyboard.css;
2045
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;
2051                 } else {
2052                         // when regKey is true, keyName is the same as action
2053                         keys = txt;
2054                         keys.action = txt.name;
2055                 }
2056
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);
2064                         }
2065                 }
2066
2067                 if (regKey) {
2068                         keyClass = data.name === '' ? '' : kbcss.keyPrefix + data.name;
2069                 } else {
2070                         // Action keys will have the 'ui-keyboard-actionkey' class
2071                         keyClass = kbcss.keyAction + ' ' + kbcss.keyPrefix + keys.action;
2072                 }
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;
2076
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) + ';';
2081                         }) +
2082                         '</span>';
2083
2084                 data.$key = base.keyBtn
2085                         .clone()
2086                         .attr({
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
2092                         })
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 &#126;)
2099                         //  See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
2100                         .addClass(keyClass)
2101                         .html(data.html)
2102                         .appendTo(base.temp[0]);
2103
2104                 if (keys.map) {
2105                         data.$key.attr('data-mapped', keys.map);
2106                 }
2107                 if (keys.title || txt.title) {
2108                         data.$key.attr({
2109                                 'data-title': txt.title || keys.title, // used to allow adding content to title
2110                                 'title': txt.title || keys.title
2111                         });
2112                 }
2113
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);
2119                 }
2120                 return data.$key;
2121         };
2122
2123         base.customHash = function (layout) {
2124                 /*jshint bitwise:false */
2125                 var i, array, hash, character, len,
2126                         arrays = [],
2127                         merged = [];
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]);
2134                         }
2135                 }
2136                 // flatten array
2137                 merged = merged.concat.apply(merged, arrays).join(' ');
2138                 // produce hash name - http://stackoverflow.com/a/7616484/145346
2139                 hash = 0;
2140                 len = merged.length;
2141                 if (len === 0) {
2142                         return hash;
2143                 }
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
2148                 }
2149                 return hash;
2150         };
2151
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();
2157                 }
2158                 var index, row, $row, currentSet,
2159                         kbcss = $keyboard.css,
2160                         sets = 0,
2161                         layout = $keyboard.builtLayouts[name || base.layout || o.layout] = {
2162                                 mappedKeys: {},
2163                                 acceptedKeys: []
2164                         },
2165                         acceptedKeys = layout.acceptedKeys = o.restrictInclude ?
2166                                 ('' + o.restrictInclude).split(/\s+/) || [] :
2167                                 [],
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 : ''),
2171
2172                         container = $('<div />')
2173                                 .addClass($layout)
2174                                 .attr({
2175                                         'role': 'textbox'
2176                                 })
2177                                 .hide();
2178
2179                 // allow adding "{space}" as an accepted key - Fixes #627
2180                 index = $.inArray('{space}', acceptedKeys);
2181                 if (index > -1) {
2182                         acceptedKeys[index] = ' ';
2183                 }
2184
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}']
2190                         };
2191                 } else {
2192                         $layout = $keyboard.layouts[internal ? o.layout : name || base.layout || o.layout];
2193                 }
2194
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') {
2201                                         set = 'normal';
2202                                 }
2203                                 sets++;
2204                                 $row = $('<div />')
2205                                         .attr('name', set) // added for typing extension
2206                                         .addClass(kbcss.keySet + ' ' + kbcss.keySet + '-' + set)
2207                                         .appendTo(container)
2208                                         .toggle(set === 'normal');
2209
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)
2215                                                 .filter(':last')
2216                                                 .after('<br class="' + kbcss.endRow + '"/>');
2217                                 }
2218                         }
2219                 });
2220
2221                 if (sets > 1) {
2222                         base.sets = true;
2223                 }
2224                 layout.hasMappedKeys = !($.isEmptyObject(layout.mappedKeys));
2225                 layout.$keyboard = container;
2226                 return container;
2227         };
2228
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];
2235                         isAction = false;
2236
2237                         // ignore empty keys
2238                         if (keys[key].length === 0) {
2239                                 continue;
2240                         }
2241
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('!!', '');
2248                                         isAction = true;
2249                                 }
2250
2251                                 // add empty space
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
2255                                                 .replace(/,/, '.')
2256                                                 .match(/^sp:((\d+)?([\.|,]\d+)?)(em|px)?$/i)[1] || 0
2257                                         );
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)
2264                                                 .appendTo($row);
2265                                 }
2266
2267                                 // add empty button
2268                                 if (/^empty(:((\d+)?([\.|,]\d+)?)(em|px)?)?$/i.test(action)) {
2269                                         margin = (/:/.test(action)) ? parseFloat(action
2270                                                 .replace(/,/, '.')
2271                                                 .match(/^empty:((\d+)?([\.|,]\d+)?)(em|px)?$/i)[1] || 0
2272                                         ) : '';
2273                                         base
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') : '');
2278                                         continue;
2279                                 }
2280
2281                                 // meta keys
2282                                 if (/^meta[\w-]+\:?(\w+)?/i.test(action)) {
2283                                         base
2284                                                 .addKey(action.split(':')[0], action)
2285                                                 .addClass(kbcss.keyHasActive);
2286                                         continue;
2287                                 }
2288
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()) {
2293
2294                                 case 'a':
2295                                 case 'accept':
2296                                         base
2297                                                 .addKey('accept', action)
2298                                                 .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
2299                                         break;
2300
2301                                 case 'alt':
2302                                 case 'altgr':
2303                                         base
2304                                                 .addKey('alt', action)
2305                                                 .addClass(kbcss.keyHasActive);
2306                                         break;
2307
2308                                 case 'b':
2309                                 case 'bksp':
2310                                         base.addKey('bksp', action);
2311                                         break;
2312
2313                                 case 'c':
2314                                 case 'cancel':
2315                                         base
2316                                                 .addKey('cancel', action)
2317                                                 .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
2318                                         break;
2319
2320                                         // toggle combo/diacritic key
2321                                         /*jshint -W083 */
2322                                 case 'combo':
2323                                         base
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'];
2329                                                 })
2330                                                 .toggleClass(o.css.buttonActive, o.useCombos);
2331                                         break;
2332
2333                                         // Decimal - unique decimal point (num pad layout)
2334                                 case 'dec':
2335                                         acceptedKeys.push((base.decimal) ? '.' : ',');
2336                                         base.addKey('dec', action);
2337                                         break;
2338
2339                                 case 'e':
2340                                 case 'enter':
2341                                         base
2342                                                 .addKey('enter', action)
2343                                                 .addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
2344                                         break;
2345
2346                                 case 'lock':
2347                                         base
2348                                                 .addKey('lock', action)
2349                                                 .addClass(kbcss.keyHasActive);
2350                                         break;
2351
2352                                 case 's':
2353                                 case 'shift':
2354                                         base
2355                                                 .addKey('shift', action)
2356                                                 .addClass(kbcss.keyHasActive);
2357                                         break;
2358
2359                                         // Change sign (for num pad layout)
2360                                 case 'sign':
2361                                         acceptedKeys.push('-');
2362                                         base.addKey('sign', action);
2363                                         break;
2364
2365                                 case 'space':
2366                                         acceptedKeys.push(' ');
2367                                         base.addKey('space', action);
2368                                         break;
2369
2370                                 case 't':
2371                                 case 'tab':
2372                                         base.addKey('tab', action);
2373                                         break;
2374
2375                                 default:
2376                                         if ($keyboard.keyaction.hasOwnProperty(txt[0])) {
2377                                                 base
2378                                                         .addKey(txt[0], action)
2379                                                         .toggleClass(o.css.buttonAction + ' ' + kbcss.keyAction, isAction);
2380                                         }
2381
2382                                 }
2383
2384                         } else {
2385
2386                                 // regular button (not an action key)
2387                                 t = keys[key];
2388                                 base.addKey(t, t, true);
2389                         }
2390                 }
2391         };
2392
2393         base.removeBindings = function (namespace) {
2394                 $(document).unbind(namespace);
2395                 if (base.el.ownerDocument !== document) {
2396                         $(base.el.ownerDocument).unbind(namespace);
2397                 }
2398                 $(window).unbind(namespace);
2399                 base.$el.unbind(namespace);
2400         };
2401
2402         base.removeKeyboard = function () {
2403                 base.$decBtn = [];
2404                 // base.$preview === base.$el when o.usePreview is false - fixes #442
2405                 if (o.usePreview) {
2406                         base.$preview.removeData('keyboard');
2407                 }
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);
2417         };
2418
2419         base.destroy = function (callback) {
2420                 var index,
2421                         kbcss = $keyboard.css,
2422                         len = base.extensionNamespace.length,
2423                         tmp = [
2424                                 kbcss.input,
2425                                 kbcss.locked,
2426                                 kbcss.placeholder,
2427                                 kbcss.noKeyboard,
2428                                 kbcss.alwaysOpen,
2429                                 o.css.input,
2430                                 kbcss.isCurrent
2431                         ].join(' ');
2432                 clearTimeout(base.timer);
2433                 clearTimeout(base.timer2);
2434                 clearTimeout(base.timer3);
2435                 if (base.$keyboard.length) {
2436                         base.removeKeyboard();
2437                 }
2438                 if (base.options.openOn) {
2439                         base.removeBindings(base.options.openOn);
2440                 }
2441                 base.removeBindings(base.namespace);
2442                 base.removeBindings(base.namespace + 'callbacks');
2443                 for (index = 0; index < len; index++) {
2444                         base.removeBindings(base.extensionNamespace[index]);
2445                 }
2446                 base.el.active = false;
2447
2448                 base.$el
2449                         .removeClass(tmp)
2450                         .removeAttr('aria-haspopup')
2451                         .removeAttr('role')
2452                         .removeData('keyboard');
2453                 base = null;
2454
2455                 if (typeof callback === 'function') {
2456                         callback();
2457                 }
2458         };
2459
2460         // Run initializer
2461         base.init();
2462
2463         }; // end $.keyboard definition
2464
2465         // event.which & ASCII values
2466         $keyboard.keyCodes = {
2467                 backSpace: 8,
2468                 tab: 9,
2469                 enter: 13,
2470                 capsLock: 20,
2471                 escape: 27,
2472                 space: 32,
2473                 pageUp: 33,
2474                 pageDown: 34,
2475                 end: 35,
2476                 home: 36,
2477                 left: 37,
2478                 up: 38,
2479                 right: 39,
2480                 down: 40,
2481                 insert: 45,
2482                 delete: 46,
2483                 // event.which keyCodes (uppercase letters)
2484                 A: 65,
2485                 Z: 90,
2486                 V: 86,
2487                 C: 67,
2488                 X: 88,
2489
2490                 // ASCII lowercase a & z
2491                 a: 97,
2492                 z: 122
2493         };
2494
2495         $keyboard.css = {
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',
2518                 // states
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>
2530         };
2531
2532         $keyboard.events = {
2533                 // keyboard events
2534                 kbChange: 'keyboardChange',
2535                 kbBeforeClose: 'beforeClose',
2536                 kbBeforeVisible: 'beforeVisible',
2537                 kbVisible: 'visible',
2538                 kbInit: 'initialized',
2539                 kbInactive: 'inactive',
2540                 kbHidden: 'hidden',
2541                 kbRepeater: 'repeater',
2542                 kbKeysetChange: 'keysetChange',
2543                 // input events
2544                 inputAccepted: 'accepted',
2545                 inputCanceled: 'canceled',
2546                 inputChange: 'change',
2547                 inputRestricted: 'restricted'
2548         };
2549
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
2555                 },
2556                 alt: function (base) {
2557                         base.altActive = !base.altActive;
2558                         base.showSet();
2559                 },
2560                 bksp: function (base) {
2561                         if (base.isContentEditable) {
2562                                 base.execCommand('delete');
2563                                 // save new caret position
2564                                 base.saveCaret();
2565                         } else {
2566                                 // the script looks for the '\b' string and initiates a backspace
2567                                 base.insertText('\b');
2568                         }
2569                 },
2570                 cancel: function (base) {
2571                         base.close();
2572                         return false; // return false prevents further processing
2573                 },
2574                 clear: function (base) {
2575                         base.$preview[base.isContentEditable ? 'text' : 'val']('');
2576                         if (base.$decBtn.length) {
2577                                 base.checkDecimal();
2578                         }
2579                 },
2580                 combo: function (base) {
2581                         var o = base.options,
2582                                 c = !o.useCombos,
2583                                 $combo = base.$keyboard.find('.' + $keyboard.css.keyPrefix + 'combo');
2584                         o.useCombos = c;
2585                         $combo
2586                                 .toggleClass(o.css.buttonActive, c)
2587                                 // update combo key state
2588                                 .attr('title', $combo.attr('data-title') + ' (' + o.display[c ? 'active' : 'disabled'] + ')');
2589                         if (c) {
2590                                 base.checkCombos();
2591                         }
2592                         return false;
2593                 },
2594                 dec: function (base) {
2595                         base.insertText((base.decimal) ? '.' : ',');
2596                 },
2597                 del: function (base) {
2598                         if (base.isContentEditable) {
2599                                 base.execCommand('forwardDelete');
2600                         } else {
2601                                 // the script looks for the '{d}' string and initiates a delete
2602                                 base.insertText('{d}');
2603                         }
2604                 },
2605                 // resets to base keyset (deprecated because "default" is a reserved word)
2606                 'default': function (base) {
2607                         base.shiftActive = base.altActive = base.metaActive = false;
2608                         base.showSet();
2609                 },
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
2614                         if (e.shiftKey) {
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);
2619                         }
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);
2623                         }
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');
2629                         }
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);
2638                                         base.saveCaret();
2639                                 }, 0);
2640                         }
2641                 },
2642                 // caps lock key
2643                 lock: function (base) {
2644                         base.last.keyset[0] = base.shiftActive = base.capsLock = !base.capsLock;
2645                         base.showSet();
2646                 },
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);
2653                                 base.setScroll();
2654                         }
2655                 },
2656                 meta: function (base, el) {
2657                         var $el = $(el);
2658                         base.metaActive = !$el.hasClass(base.options.css.buttonActive);
2659                         base.showSet($el.attr('data-name'));
2660                 },
2661                 next: function (base) {
2662                         base.switchInput(true, base.options.autoAccept);
2663                         return false;
2664                 },
2665                 // same as 'default' - resets to base keyset
2666                 normal: function (base) {
2667                         base.shiftActive = base.altActive = base.metaActive = false;
2668                         base.showSet();
2669                 },
2670                 prev: function (base) {
2671                         base.switchInput(false, base.options.autoAccept);
2672                         return false;
2673                 },
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);
2682                                 base.setScroll();
2683                         }
2684                 },
2685                 shift: function (base) {
2686                         base.last.keyset[0] = base.shiftActive = !base.shiftActive;
2687                         base.showSet();
2688                 },
2689                 sign: function (base) {
2690                         if (/^[+-]?\d*\.?\d*$/.test(base.getValue())) {
2691                                 var caret,
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);
2700                                 base.setScroll();
2701                         }
2702                 },
2703                 space: function (base) {
2704                         base.insertText(' ');
2705                 },
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
2713                                         return false;
2714                                 }
2715                         }
2716                         base.insertText('\t');
2717                 },
2718                 toggle: function (base) {
2719                         base.enabled = !base.enabled;
2720                         base.toggle();
2721                 },
2722                 // *** Special action keys: NBSP & zero-width characters ***
2723                 // Non-breaking space
2724                 NBSP: '\u00a0',
2725                 // zero width space
2726                 ZWSP: '\u200b',
2727                 // Zero width non-joiner
2728                 ZWNJ: '\u200c',
2729                 // Zero width joiner
2730                 ZWJ: '\u200d',
2731                 // Left-to-right Mark
2732                 LRM: '\u200e',
2733                 // Right-to-left Mark
2734                 RLM: '\u200f'
2735         };
2736
2737         // Default keyboard layouts
2738         $keyboard.builtLayouts = {};
2739         $keyboard.layouts = {
2740                 'alpha': {
2741                         'normal': [
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}'
2747                         ],
2748                         'shift': [
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}'
2754                         ]
2755                 },
2756                 'qwerty': {
2757                         'normal': [
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}'
2763                         ],
2764                         'shift': [
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}'
2770                         ]
2771                 },
2772                 'international': {
2773                         'normal': [
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}'
2779                         ],
2780                         'shift': [
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}'
2786                         ],
2787                         'alt': [
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}'
2793                         ],
2794                         'alt-shift': [
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}'
2800                         ]
2801                 },
2802                 'colemak': {
2803                         'normal': [
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}'
2809                         ],
2810                         'shift': [
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}'
2816                         ]
2817                 },
2818                 'dvorak': {
2819                         'normal': [
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}'
2825                         ],
2826                         'shift': [
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}'
2832                         ]
2833                 },
2834                 'num': {
2835                         'normal': [
2836                                 '= ( ) {b}',
2837                                 '{clear} / * -',
2838                                 '7 8 9 +',
2839                                 '4 5 6 {sign}',
2840                                 '1 2 3 %',
2841                                 '0 {dec} {a} {c}'
2842                         ]
2843                 }
2844         };
2845
2846         $keyboard.language = {
2847                 en: {
2848                         display: {
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 &larr;)
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)',
2860                                 // clear num pad
2861                                 'clear': 'C:Clear',
2862                                 'combo': '\u00f6:Toggle Combo Keys',
2863                                 // decimal point for num pad (optional), change '.' to ',' for European format
2864                                 'dec': '.:Decimal',
2865                                 // down, then left arrow - enter symbol
2866                                 'e': '\u23ce:Enter',
2867                                 'empty': '\u00a0',
2868                                 'enter': 'Enter:Enter \u23ce',
2869                                 // left arrow (move caret)
2870                                 'left': '\u2190',
2871                                 // caps lock
2872                                 'lock': 'Lock:\u21ea Caps Lock',
2873                                 'next': 'Next \u21e8',
2874                                 'prev': '\u21e6 Prev',
2875                                 // right arrow (move caret)
2876                                 'right': '\u2192',
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)
2884                                 't': '\u21e5:Tab',
2885                                 // \u21b9 is the true tab symbol (left & right arrows)
2886                                 'tab': '\u21e5 Tab:Tab',
2887                                 // replaced by an image
2888                                 'toggle': ' ',
2889
2890                                 // added to titles of keys
2891                                 // accept key status when acceptValid:true
2892                                 'valid': 'valid',
2893                                 'invalid': 'invalid',
2894                                 // combo key states
2895                                 'active': 'active',
2896                                 'disabled': 'disabled'
2897                         },
2898
2899                         // Message added to the key title while hovering, if the mousewheel plugin exists
2900                         wheelMessage: 'Use mousewheel to see other keys',
2901
2902                         comboRegex: /([`\'~\^\"ao])([a-z])/mig,
2903                         combos: {
2904                                 // grave
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' },
2907                                 // acute & cedilla
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' },
2910                                 // umlaut/trema
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' },
2913                                 // circumflex
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' },
2916                                 // tilde
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' }
2919                         }
2920                 }
2921         };
2922
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
2927                 language: null,
2928                 rtl: false,
2929
2930                 // *** choose layout & positioning ***
2931                 layout: 'qwerty',
2932                 customLayout: null,
2933
2934                 position: {
2935                         // optional - null (attach to input/textarea) or a jQuery object (attach elsewhere)
2936                         of: null,
2937                         my: 'center top',
2938                         at: 'center top',
2939                         // used when 'usePreview' is false (centers the keyboard at the bottom of the input/textarea)
2940                         at2: 'center bottom'
2941                 },
2942
2943                 // allow jQuery position utility to reposition the keyboard on window resize
2944                 reposition: true,
2945
2946                 // preview added above keyboard if true, original input/textarea used if false
2947                 usePreview: true,
2948
2949                 // if true, the keyboard will always be visible
2950                 alwaysOpen: false,
2951
2952                 // give the preview initial focus when the keyboard becomes visible
2953                 initialFocus: true,
2954
2955                 // avoid changing the focus (hardware keyboard probably won't work)
2956                 noFocus: false,
2957
2958                 // if true, keyboard will remain open even if the input loses focus, but closes on escape
2959                 // or when another keyboard opens.
2960                 stayOpen: false,
2961
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
2964                 userClosed: false,
2965
2966                 // if true, keyboard will not close if you press escape.
2967                 ignoreEsc: false,
2968
2969                 // if true, keyboard will only closed on click event instead of mousedown and touchstart
2970                 closeByClickEvent: false,
2971
2972                 css: {
2973                         // input & preview
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)
2978                         popup: '',
2979                         // default state
2980                         buttonDefault: 'ui-state-default ui-corner-all',
2981                         // hovered button
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'
2990                 },
2991
2992                 // *** Useability ***
2993                 // Auto-accept content when clicking outside the keyboard (popup will close)
2994                 autoAccept: false,
2995                 // Auto-accept content even if the user presses escape (only works if `autoAccept` is `true`)
2996                 autoAcceptOnEsc: false,
2997
2998                 // Prevents direct input in the preview window when true
2999                 lockInput: false,
3000
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'
3005
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'
3009                 acceptValid: false,
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,
3015
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
3018                 cancelClose: true,
3019
3020                 // tab to go to next, shift-tab for previous (default behavior)
3021                 tabNavigation: false,
3022
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
3029
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
3032                 stopAtEnd: true,
3033
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'
3036                 // option is true.
3037                 appendLocally: false,
3038                 // When appendLocally is false, the keyboard will be appended to this object
3039                 appendTo: 'body',
3040
3041                 // Wrap all <br>s inside of a contenteditable in a div; without wrapping, the caret
3042                 // position will not be accurate
3043                 wrapBRs: true,
3044
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
3047                 stickyShift: true,
3048
3049                 // Prevent pasting content into the area
3050                 preventPaste: false,
3051
3052                 // caret placed at the end of any text when keyboard becomes visible
3053                 caretToEnd: false,
3054
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,
3058
3059                 // Set the max number of characters allowed in the input, setting it to false disables this option
3060                 maxLength: false,
3061                 // allow inserting characters @ caret when maxLength is set
3062                 maxInsert: true,
3063
3064                 // Mouse repeat delay - when clicking/touching a virtual keyboard key, after this delay the key will
3065                 // start repeating
3066                 repeatDelay: 500,
3067
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.
3072                 repeatRate: 20,
3073
3074                 // resets the keyboard to the default keyset when visible
3075                 resetDefault: true,
3076
3077                 // Event (namespaced) on the input to reveal the keyboard. To disable it, just set it to ''.
3078                 openOn: 'focus',
3079
3080                 // enable the keyboard on readonly inputs
3081                 activeOnReadonly: false,
3082
3083                 // Event (namepaced) for when the character is added to the input (clicking on the keyboard)
3084                 keyBinding: 'mousedown touchstart',
3085
3086                 // enable/disable mousewheel functionality
3087                 // enabling still depends on the mousewheel plugin
3088                 useWheel: true,
3089
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.
3092                 useCombos: true,
3093
3094                 /*
3095                         // *** Methods ***
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(); },
3112
3113                         // build key callback
3114                         buildKey : function( keyboard, data ) {
3115                                 / *
3116                                 data = {
3117                                 // READ ONLY
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
3127                                 }
3128                                 * /
3129                                 return data;
3130                         },
3131                 */
3132
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 */) {
3140                         return true;
3141                 }
3142
3143         };
3144
3145         // for checking combos
3146         $keyboard.comboRegex = /([`\'~\^\"ao])([a-z])/mig;
3147
3148         // store current keyboard element; used by base.isCurrent()
3149         $keyboard.currentKeyboard = '';
3150
3151         $('<!--[if lte IE 8]><script>jQuery("body").addClass("oldie");</script><![endif]--><!--[if IE]>' +
3152                         '<script>jQuery("body").addClass("ie");</script><![endif]-->')
3153                 .appendTo('body')
3154                 .remove();
3155         $keyboard.msie = $('body').hasClass('oldie'); // Old IE flag, used for caret positioning
3156         $keyboard.allie = $('body').hasClass('ie');
3157
3158         $keyboard.watermark = (typeof (document.createElement('input').placeholder) !== 'undefined');
3159
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;
3169                         $temp.remove();
3170                 }
3171                 return $keyboard.checkCaret;
3172         };
3173
3174         $keyboard.caret = function($el, param1, param2) {
3175                 if (!$el || !$el.length || $el.is(':hidden') || $el.css('visibility') === 'hidden') {
3176                         return {};
3177                 }
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;
3188                                 end = param1.end;
3189                         } else if (typeof param2 === 'undefined') {
3190                                 param2 = param1; // set caret using start position
3191                         }
3192                         // set caret using ( $el, start, end );
3193                         if (typeof param1 === 'number' && typeof param2 === 'number') {
3194                                 start = param1;
3195                                 end = param2;
3196                         } else if ( param1 === 'start' ) {
3197                                 start = end = 0;
3198                         } else if ( typeof param1 === 'string' ) {
3199                                 // unknown string setting, move caret to end
3200                                 start = end = 'end';
3201                         }
3202
3203                         // *** SET CARET POSITION ***
3204                         // modify the line below to adapt to other caret plugins
3205                         return formEl ?
3206                                 $el.caret( start, end, noFocus ) :
3207                                 $keyboard.setEditableCaret( $el, start, end );
3208                 }
3209                 // *** GET CARET POSITION ***
3210                 // modify the line below to adapt to other caret plugins
3211                 if (formEl) {
3212                         // modify the line below to adapt to other caret plugins
3213                         pos = $el.caret();
3214                 } else {
3215                         // contenteditable
3216                         pos = $keyboard.getEditableCaret($el[0]);
3217                 }
3218                 start = pos.start;
3219                 end = pos.end;
3220
3221                 // *** utilities ***
3222                 txt = formEl && $el[0].value || $el.text() || '';
3223                 return {
3224                         start : start,
3225                         end : end,
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 );
3231                         }
3232                 };
3233         };
3234
3235         $keyboard.isTextNode = function(el) {
3236                 return el && el.nodeType === 3;
3237         };
3238
3239         $keyboard.isBlock = function(el, node) {
3240                 var win = el.ownerDocument.defaultView;
3241                 if (
3242                         node && node.nodeType === 1 && node !== el &&
3243                         win.getComputedStyle(node).display === 'block'
3244                 ) {
3245                         return 1;
3246                 }
3247                 return 0;
3248         };
3249
3250         // Wrap all BR's inside of contenteditable
3251         $keyboard.wrapBRs = function(container) {
3252                 var $el = $(container).find('br:not(.' + $keyboard.css.divWrapperCE + ')');
3253                 if ($el.length) {
3254                         $.each($el, function(i, el) {
3255                                 var len = el.parentNode.childNodes.length;
3256                                 if (
3257                                         // wrap BRs if not solo child
3258                                         len !== 1 ||
3259                                         // Or if BR is wrapped by a span
3260                                         len === 1 && !$keyboard.isBlock(container, el.parentNode)
3261                                 ) {
3262                                         $(el).addClass($keyboard.css.divWrapperCE).wrap('<div>');
3263                                 }
3264                         });
3265                 }
3266         };
3267
3268         $keyboard.getEditableCaret = function(container) {
3269                 container = $(container)[0];
3270                 if (!container.isContentEditable) { return {}; }
3271                 var end, text,
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);
3279                 }
3280                 function pathToNode(endNode, offset) {
3281                         var node, adjust,
3282                                 txt = '',
3283                                 done = false,
3284                                 position = 0,
3285                                 nodes = $.makeArray(container.childNodes);
3286
3287                         function checkBlock(val) {
3288                                 if (val) {
3289                                         position += val;
3290                                         txt += options && options.replaceCR || '\n';
3291                                 }
3292                         }
3293
3294                         while (!done && nodes.length) {
3295                                 node = nodes.shift();
3296                                 if (node === endNode) {
3297                                         done = true;
3298                                 }
3299
3300                                 // Add one if previous sibling was a block node (div, p, etc)
3301                                 adjust = $keyboard.isBlock(container, node.previousSibling);
3302                                 checkBlock(adjust);
3303
3304                                 if ($keyboard.isTextNode(node)) {
3305                                         position += done ? offset : node.length;
3306                                         txt += node.textContent;
3307                                         if (done) {
3308                                                 return {position: position, text: txt};
3309                                         }
3310                                 } else if (!done && node.childNodes) {
3311                                         nodes = $.makeArray(node.childNodes).concat(nodes);
3312                                 }
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);
3316                                 checkBlock(adjust);
3317                         }
3318                         return {position: position, text: txt};
3319                 }
3320                 // check of start and end are the same
3321                 if (range.endContainer === range.startContainer && range.endOffset === range.startOffset) {
3322                         end = start;
3323                         text = '';
3324                 } else {
3325                         result = pathToNode(range.endContainer, range.endOffset);
3326                         end = result.position;
3327                         text = result.text.substring(start, end);
3328                 }
3329                 return {
3330                         start: start,
3331                         end: end,
3332                         text: text
3333                 };
3334         };
3335
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;
3340         };
3341
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,
3349                         s = start,
3350                         e = end,
3351                         text = '',
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) {
3358                                 offset = 0;
3359                         }
3360                         var node, check,
3361                                 txt = '',
3362                                 done = false,
3363                                 position = 0,
3364                                 last = 0,
3365                                 max = 0,
3366                                 nodes = $.makeArray(container.childNodes);
3367                         function updateText(val) {
3368                                 txt += val ? options && options.replaceCR || '\n' : '';
3369                                 return val > 0;
3370                         }
3371                         function checkDone(adj) {
3372                                 var val = position + adj;
3373                                 last = max;
3374                                 max += adj;
3375                                 if (offset - val >= 0) {
3376                                         position = val;
3377                                         return offset - position <= 0;
3378                                 }
3379                                 return offset - val <= 0;
3380                         }
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)) {
3386                                         done = true;
3387                                 }
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)) {
3391                                         done = true;
3392                                 }
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;
3397                                                 return {
3398                                                         node: node,
3399                                                         offset: check,
3400                                                         position: offset,
3401                                                         text: txt
3402                                                 };
3403                                         }
3404                                 } else if (!done && node.childNodes) {
3405                                         nodes = $.makeArray(node.childNodes).concat(nodes);
3406                                 }
3407                         }
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};
3412                 }
3413                 if (result.node) {
3414                         s = result.position; // Adjust if start > content length
3415                         if (start === 'getMax') {
3416                                 return s;
3417                         }
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);
3423                         }
3424                         if (result.node) {
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);
3428                         }
3429                         sel.removeAllRanges();
3430                         sel.addRange(range);
3431                 }
3432                 return {
3433                         start: s,
3434                         end: e,
3435                         text: text
3436                 };
3437         };
3438
3439         $keyboard.replaceContent = function (el, param) {
3440                 el = $(el)[0];
3441                 var node, i, str,
3442                         type = typeof param,
3443                         caret = $keyboard.getEditableCaret(el).start,
3444                         charIndex = 0,
3445                         nodeStack = [el];
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);
3451                                         }
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;
3457                                         }
3458                                 }
3459                                 charIndex += node.length;
3460                         } else if (node && node.childNodes) {
3461                                 i = node.childNodes.length;
3462                                 while (i--) {
3463                                         nodeStack.push(node.childNodes[i]);
3464                                 }
3465                         }
3466                 }
3467                 i = $keyboard.getEditableCaret(el);
3468                 $keyboard.setEditableCaret(el, i.start, i.start);
3469         };
3470
3471         $.fn.keyboard = function (options) {
3472                 return this.each(function () {
3473                         if (!$(this).data('keyboard')) {
3474                                 /*jshint nonew:false */
3475                                 (new $.keyboard(this, options));
3476                         }
3477                 });
3478         };
3479
3480         $.fn.getkeyboard = function () {
3481                 return this.data('keyboard');
3482         };
3483
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
3488          */
3489
3490         $.fn.caret = function (start, end, noFocus) {
3491                 if (
3492                         typeof this[0] === 'undefined' ||
3493                         this.is(':hidden') ||
3494                         this.css('visibility') === 'hidden' ||
3495                         !/(INPUT|TEXTAREA)/i.test(this[0].nodeName)
3496                 ) {
3497                         return this;
3498                 }
3499                 var selRange, range, stored_range, txt, val,
3500                         $el = this,
3501                         el = $el[0],
3502                         selection = el.ownerDocument.selection,
3503                         sTop = el.scrollTop,
3504                         ss = false,
3505                         supportCaret = true;
3506                 try {
3507                         ss = 'selectionStart' in el;
3508                 } catch (err) {
3509                         supportCaret = false;
3510                 }
3511                 if (supportCaret && typeof start !== 'undefined') {
3512                         if (!/(email|number)/i.test(el.type)) {
3513                                 if (ss) {
3514                                         el.selectionStart = start;
3515                                         el.selectionEnd = end;
3516                                 } else {
3517                                         selRange = el.createTextRange();
3518                                         selRange.collapse(true);
3519                                         selRange.moveStart('character', start);
3520                                         selRange.moveEnd('character', end - start);
3521                                         selRange.select();
3522                                 }
3523                         }
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')) {
3526                                 el.focus();
3527                         }
3528                         el.scrollTop = sTop;
3529                         return this;
3530                 }
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;
3534                 } else if (ss) {
3535                         start = el.selectionStart;
3536                         end = el.selectionEnd;
3537                 } else if (selection) {
3538                         if (el.nodeName.toUpperCase() === 'TEXTAREA') {
3539                                 val = $el.val();
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;
3547                         } else {
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;
3555                         }
3556                 } else {
3557                         // caret positioning not supported
3558                         start = end = (el.value || '').length;
3559                 }
3560                 txt = (el.value || '');
3561                 return {
3562                         start: start,
3563                         end: end,
3564                         text: txt.substring(start, end),
3565                         replace: function (str) {
3566                                 return txt.substring(0, start) + str + txt.substring(end, txt.length);
3567                         }
3568                 };
3569         };
3570
3571         return $keyboard;
3572
3573 }));