/*! jQuery UI Virtual Keyboard v1.27.3 *//* Author: Jeremy Satterfield Maintained: Rob Garrison (Mottie on github) Licensed under the MIT License An on-screen virtual keyboard embedded within the browser window which will popup when a specified entry field is focused. The user can then type and preview their input before Accepting or Canceling. This plugin adds default class names to match jQuery UI theme styling. Bootstrap & custom themes may also be applied - See https://github.com/Mottie/Keyboard#themes Requires: jQuery v1.4.3+ Caret plugin (included) Optional: jQuery UI (position utility only) & CSS theme jQuery mousewheel Setup/Usage: Please refer to https://github.com/Mottie/Keyboard/wiki ----------------------------------------- Caret code modified from jquery.caret.1.02.js Licensed under the MIT License: http://www.opensource.org/licenses/mit-license.php ----------------------------------------- */ /*jshint browser:true, jquery:true, unused:false */ /*global require:false, define:false, module:false */ ;(function (factory) { if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else if (typeof module === 'object' && typeof module.exports === 'object') { module.exports = factory(require('jquery')); } else { factory(jQuery); } }(function ($) { 'use strict'; var $keyboard = $.keyboard = function (el, options) { var o, base = this; base.version = '1.27.3'; // Access to jQuery and DOM versions of element base.$el = $(el); base.el = el; // Add a reverse reference to the DOM object base.$el.data('keyboard', base); base.init = function () { base.initialized = false; var k, position, tmp, kbcss = $keyboard.css, kbevents = $keyboard.events; base.settings = options || {}; // shallow copy position to prevent performance issues; see #357 if (options && options.position) { position = $.extend({}, options.position); options.position = null; } base.options = o = $.extend(true, {}, $keyboard.defaultOptions, options); if (position) { o.position = position; options.position = position; } // keyboard is active (not destroyed); base.el.active = true; // unique keyboard namespace base.namespace = '.keyboard' + Math.random().toString(16).slice(2); // extension namespaces added here (to unbind listeners on base.$el upon destroy) base.extensionNamespace = []; // Shift and Alt key toggles, sets is true if a layout has more than one keyset // used for mousewheel message base.shiftActive = base.altActive = base.metaActive = base.sets = base.capsLock = false; // Class names of the basic key set - meta keysets are handled by the keyname base.rows = ['', '-shift', '-alt', '-alt-shift']; base.inPlaceholder = base.$el.attr('placeholder') || ''; // html 5 placeholder/watermark base.watermark = $keyboard.watermark && base.inPlaceholder !== ''; // convert mouse repeater rate (characters per second) into a time in milliseconds. base.repeatTime = 1000 / (o.repeatRate || 20); // delay in ms to prevent mousedown & touchstart from both firing events at the same time o.preventDoubleEventTime = o.preventDoubleEventTime || 100; // flag indication that a keyboard is open base.isOpen = false; // is mousewheel plugin loaded? base.wheel = $.isFunction($.fn.mousewheel); // special character in regex that need to be escaped base.escapeRegex = /[-\/\\^$*+?.()|[\]{}]/g; // detect contenteditable base.isContentEditable = !/(input|textarea)/i.test(base.el.nodeName) && base.el.isContentEditable; // keyCode of keys always allowed to be typed k = $keyboard.keyCodes; // base.alwaysAllowed = [20,33,34,35,36,37,38,39,40,45,46]; base.alwaysAllowed = [ k.capsLock, k.pageUp, k.pageDown, k.end, k.home, k.left, k.up, k.right, k.down, k.insert, k.delete ]; base.$keyboard = []; // keyboard enabled; set to false on destroy base.enabled = true; base.checkCaret = (o.lockInput || $keyboard.checkCaretSupport()); // disable problematic usePreview for contenteditable if (base.isContentEditable) { o.usePreview = false; } base.last = { start: 0, end: 0, key: '', val: '', preVal: '', layout: '', virtual: true, keyset: [false, false, false], // [shift, alt, meta] wheel_$Keys: null, wheelIndex: 0, wheelLayers: [] }; // used when building the keyboard - [keyset element, row, index] base.temp = ['', 0, 0]; // Callbacks $.each([ kbevents.kbInit, kbevents.kbBeforeVisible, kbevents.kbVisible, kbevents.kbHidden, kbevents.inputCanceled, kbevents.inputAccepted, kbevents.kbBeforeClose, kbevents.inputRestricted ], function (i, callback) { if ($.isFunction(o[callback])) { // bind callback functions within options to triggered events base.$el.bind(callback + base.namespace + 'callbacks', o[callback]); } }); // Close with esc key & clicking outside if (o.alwaysOpen) { o.stayOpen = true; } tmp = $(document); if (base.el.ownerDocument !== document) { tmp = tmp.add(base.el.ownerDocument); } var bindings = 'keyup checkkeyboard mousedown touchstart '; if (o.closeByClickEvent) { bindings += 'click '; } // debounce bindings... see #542 tmp.bind(bindings.split(' ').join(base.namespace + ' '), function(e) { clearTimeout(base.timer3); base.timer3 = setTimeout(function() { base.checkClose(e); }, 1); }); // Display keyboard on focus base.$el .addClass(kbcss.input + ' ' + o.css.input) .attr({ 'aria-haspopup': 'true', 'role': 'textbox' }); // set lockInput if the element is readonly; or make the element readonly if lockInput is set if (o.lockInput || base.el.readOnly) { o.lockInput = true; base.$el .addClass(kbcss.locked) .attr({ 'readonly': 'readonly' }); } // add disabled/readonly class - dynamically updated on reveal if (base.$el.is(':disabled') || (base.$el.attr('readonly') && !base.$el.hasClass(kbcss.locked))) { base.$el.addClass(kbcss.noKeyboard); } if (o.openOn) { base.bindFocus(); } // Add placeholder if not supported by the browser if ( !base.watermark && base.getValue(base.$el) === '' && base.inPlaceholder !== '' && base.$el.attr('placeholder') !== '' ) { // css watermark style (darker text) base.$el.addClass(kbcss.placeholder); base.setValue(base.inPlaceholder, base.$el); } base.$el.trigger(kbevents.kbInit, [base, base.el]); // initialized with keyboard open if (o.alwaysOpen) { base.reveal(); } base.initialized = true; }; base.toggle = function () { if (!base.hasKeyboard()) { return; } var $toggle = base.$keyboard.find('.' + $keyboard.css.keyToggle), locked = !base.enabled; // prevent physical keyboard from working base.preview.readonly = locked || base.options.lockInput; // disable all buttons base.$keyboard .toggleClass($keyboard.css.keyDisabled, locked) .find('.' + $keyboard.css.keyButton) .not($toggle) .attr('aria-disabled', locked) .each(function() { this.disabled = locked; }); $toggle.toggleClass($keyboard.css.keyDisabled, locked); // stop auto typing if (locked && base.typing_options) { base.typing_options.text = ''; } // allow chaining return base; }; base.setCurrent = function () { var kbcss = $keyboard.css, // close any "isCurrent" keyboard (just in case they are always open) $current = $('.' + kbcss.isCurrent), kb = $current.data('keyboard'); // close keyboard, if not self if (!$.isEmptyObject(kb) && kb.el !== base.el) { kb.close(kb.options.autoAccept ? 'true' : false); } $current.removeClass(kbcss.isCurrent); // ui-keyboard-has-focus is applied in case multiple keyboards have // alwaysOpen = true and are stacked $('.' + kbcss.hasFocus).removeClass(kbcss.hasFocus); base.$el.addClass(kbcss.isCurrent); base.$keyboard.addClass(kbcss.hasFocus); base.isCurrent(true); base.isOpen = true; }; base.isCurrent = function (set) { var cur = $keyboard.currentKeyboard || false; if (set) { cur = $keyboard.currentKeyboard = base.el; } else if (set === false && cur === base.el) { cur = $keyboard.currentKeyboard = ''; } return cur === base.el; }; base.hasKeyboard = function () { return base.$keyboard && base.$keyboard.length > 0; }; base.isVisible = function () { return base.hasKeyboard() ? base.$keyboard.is(':visible') : false; }; base.setFocus = function () { var $el = base.$preview || base.$el; if (!o.noFocus) { $el.focus(); } if (base.isContentEditable) { $keyboard.setEditableCaret($el, base.last.start, base.last.end); } else { $keyboard.caret($el, base.last); } }; base.focusOn = function () { if (!base && base.el.active) { // keyboard was destroyed return; } if (!base.isVisible()) { clearTimeout(base.timer); base.reveal(); } else { // keyboard already open, make it the current keyboard base.setCurrent(); } }; // add redraw method to make API more clear base.redraw = function (layout) { if (layout) { // allow updating the layout by calling redraw base.options.layout = layout; } // update keyboard after a layout change if (base.$keyboard.length) { base.last.preVal = '' + base.last.val; base.saveLastChange(); base.setValue(base.last.val, base.$el); base.removeKeyboard(); base.shiftActive = base.altActive = base.metaActive = false; } base.isOpen = o.alwaysOpen; base.reveal(true); return base; }; base.reveal = function (redraw) { var temp, alreadyOpen = base.isOpen, kbcss = $keyboard.css; base.opening = !alreadyOpen; // remove all 'extra' keyboards by calling close function $('.' + kbcss.keyboard).not('.' + kbcss.alwaysOpen).each(function(){ var kb = $(this).data('keyboard'); if (!$.isEmptyObject(kb)) { // this closes previous keyboard when clicking another input - see #515 kb.close(kb.options.autoAccept ? 'true' : false); } }); // Don't open if disabled if (base.$el.is(':disabled') || (base.$el.attr('readonly') && !base.$el.hasClass(kbcss.locked))) { base.$el.addClass(kbcss.noKeyboard); return; } else { base.$el.removeClass(kbcss.noKeyboard); } // Unbind focus to prevent recursion - openOn may be empty if keyboard is opened externally if (o.openOn) { base.$el.unbind($.trim((o.openOn + ' ').split(/\s+/).join(base.namespace + ' '))); } // build keyboard if it doesn't exist; or attach keyboard if it was removed, but not cleared if (!base.$keyboard || base.$keyboard && (!base.$keyboard.length || $.contains(base.el.ownerDocument.body, base.$keyboard[0]))) { base.startup(); } // clear watermark if (!base.watermark && base.getValue() === base.inPlaceholder) { base.$el.removeClass(kbcss.placeholder); base.setValue('', base.$el); } // save starting content, in case we cancel base.originalContent = base.isContentEditable ? base.$el.html() : base.getValue(base.$el); if (base.el !== base.preview && !base.isContentEditable) { base.setValue(base.originalContent); } // disable/enable accept button if (o.acceptValid) { base.checkValid(); } if (o.resetDefault) { base.shiftActive = base.altActive = base.metaActive = false; } base.showSet(); // beforeVisible event if (!base.isVisible()) { base.$el.trigger($keyboard.events.kbBeforeVisible, [base, base.el]); } if ( base.initialized || o.initialFocus || ( !o.initialFocus && base.$el.hasClass($keyboard.css.initialFocus) ) ) { base.setCurrent(); } // update keyboard - enabled or disabled? base.toggle(); // show keyboard base.$keyboard.show(); // adjust keyboard preview window width - save width so IE won't keep expanding (fix issue #6) if (o.usePreview && $keyboard.msie) { if (typeof base.width === 'undefined') { base.$preview.hide(); // preview is 100% browser width in IE7, so hide the damn thing base.width = Math.ceil(base.$keyboard.width()); // set input width to match the widest keyboard row base.$preview.show(); } base.$preview.width(base.width); } base.reposition(); base.checkDecimal(); // get preview area line height // add roughly 4px to get line height from font height, works well for font-sizes from 14-36px // needed for textareas base.lineHeight = parseInt(base.$preview.css('lineHeight'), 10) || parseInt(base.$preview.css('font-size'), 10) + 4; if (o.caretToEnd) { temp = base.isContentEditable ? $keyboard.getEditableLength(base.el) : base.originalContent.length; base.saveCaret(temp, temp); } // IE caret haxx0rs if ($keyboard.allie) { // sometimes end = 0 while start is > 0 if (base.last.end === 0 && base.last.start > 0) { base.last.end = base.last.start; } // IE will have start -1, end of 0 when not focused (see demo: https://jsfiddle.net/Mottie/fgryQ/3/) if (base.last.start < 0) { // ensure caret is at the end of the text (needed for IE) base.last.start = base.last.end = base.originalContent.length; } } if (alreadyOpen || redraw) { // restore caret position (userClosed) $keyboard.caret(base.$preview, base.last); return base; } // opening keyboard flag; delay allows switching between keyboards without immediately closing // the keyboard base.timer2 = setTimeout(function () { var undef; base.opening = false; // Number inputs don't support selectionStart and selectionEnd // Number/email inputs don't support selectionStart and selectionEnd if (!/(number|email)/i.test(base.el.type) && !o.caretToEnd) { // caret position is always 0,0 in webkit; and nothing is focused at this point... odd // save caret position in the input to transfer it to the preview // inside delay to get correct caret position base.saveCaret(undef, undef, base.$el); } if (o.initialFocus || base.$el.hasClass($keyboard.css.initialFocus)) { $keyboard.caret(base.$preview, base.last); } // save event time for keyboards with stayOpen: true base.last.eventTime = new Date().getTime(); base.$el.trigger($keyboard.events.kbVisible, [base, base.el]); base.timer = setTimeout(function () { // get updated caret information after visible event - fixes #331 if (base) { // Check if base exists, this is a case when destroy is called, before timers fire base.saveCaret(); } }, 200); }, 10); // return base to allow chaining in typing extension return base; }; base.updateLanguage = function () { // change language if layout is named something like 'french-azerty-1' var layouts = $keyboard.layouts, lang = o.language || layouts[o.layout] && layouts[o.layout].lang && layouts[o.layout].lang || [o.language || 'en'], kblang = $keyboard.language; // some languages include a dash, e.g. 'en-gb' or 'fr-ca' // allow o.language to be a string or array... // array is for future expansion where a layout can be set for multiple languages lang = ($.isArray(lang) ? lang[0] : lang); base.language = lang; lang = lang.split('-')[0]; // set keyboard language o.display = $.extend(true, {}, kblang.en.display, kblang[lang] && kblang[lang].display || {}, base.settings.display ); o.combos = $.extend(true, {}, kblang.en.combos, kblang[lang] && kblang[lang].combos || {}, base.settings.combos ); o.wheelMessage = kblang[lang] && kblang[lang].wheelMessage || kblang.en.wheelMessage; // rtl can be in the layout or in the language definition; defaults to false o.rtl = layouts[o.layout] && layouts[o.layout].rtl || kblang[lang] && kblang[lang].rtl || false; // save default regex (in case loading another layout changes it) base.regex = kblang[lang] && kblang[lang].comboRegex || $keyboard.comboRegex; // determine if US '.' or European ',' system being used base.decimal = /^\./.test(o.display.dec); base.$el .toggleClass('rtl', o.rtl) .css('direction', o.rtl ? 'rtl' : ''); }; base.startup = function () { var kbcss = $keyboard.css; // ensure base.$preview is defined; but don't overwrite it if keyboard is always visible if (!((o.alwaysOpen || o.userClosed) && base.$preview)) { base.makePreview(); } if (!base.hasKeyboard()) { // custom layout - create a unique layout name based on the hash if (o.layout === 'custom') { o.layoutHash = 'custom' + base.customHash(); } base.layout = o.layout === 'custom' ? o.layoutHash : o.layout; base.last.layout = base.layout; base.updateLanguage(); if (typeof $keyboard.builtLayouts[base.layout] === 'undefined') { if ($.isFunction(o.create)) { // create must call buildKeyboard() function; or create it's own keyboard base.$keyboard = o.create(base); } else if (!base.$keyboard.length) { base.buildKeyboard(base.layout, true); } } base.$keyboard = $keyboard.builtLayouts[base.layout].$keyboard.clone(); base.$keyboard.data('keyboard', base); if ((base.el.id || '') !== '') { // add ID to keyboard for styling purposes base.$keyboard.attr('id', base.el.id + $keyboard.css.idSuffix); } base.makePreview(); } // Add layout and laguage data-attibutes base.$keyboard .attr('data-' + kbcss.keyboard + '-layout', o.layout) .attr('data-' + kbcss.keyboard + '-language', base.language); base.$decBtn = base.$keyboard.find('.' + kbcss.keyPrefix + 'dec'); // add enter to allowed keys; fixes #190 if (o.enterNavigation || base.el.nodeName === 'TEXTAREA') { base.alwaysAllowed.push(13); } base.bindKeyboard(); base.$keyboard.appendTo(o.appendLocally ? base.$el.parent() : o.appendTo || 'body'); base.bindKeys(); // reposition keyboard on window resize if (o.reposition && $.ui && $.ui.position && o.appendTo == 'body') { $(window).bind('resize' + base.namespace, function () { base.reposition(); }); } }; base.reposition = function () { base.position = $.isEmptyObject(o.position) ? false : o.position; // position after keyboard is visible (required for UI position utility) // and appropriately sized if ($.ui && $.ui.position && base.position) { base.position.of = // get single target position base.position.of || // OR target stored in element data (multiple targets) base.$el.data('keyboardPosition') || // OR default @ element base.$el; base.position.collision = base.position.collision || 'flipfit flipfit'; base.position.at = o.usePreview ? o.position.at : o.position.at2; if (base.isVisible()) { base.$keyboard.position(base.position); } } // make chainable return base; }; base.makePreview = function () { if (o.usePreview) { var indx, attrs, attr, removedAttr, kbcss = $keyboard.css; base.$preview = base.$el.clone(false) .data('keyboard', base) .removeClass(kbcss.placeholder + ' ' + kbcss.input) .addClass(kbcss.preview + ' ' + o.css.input) .attr('tabindex', '-1') .show(); // for hidden inputs base.preview = base.$preview[0]; // Switch the number input field to text so the caret positioning will work again if (base.preview.type === 'number') { base.preview.type = 'text'; } // remove extraneous attributes. removedAttr = /^(data-|id|aria-haspopup)/i; attrs = base.$preview.get(0).attributes; for (indx = attrs.length - 1; indx >= 0; indx--) { attr = attrs[indx] && attrs[indx].name; if (removedAttr.test(attr)) { // remove data-attributes - see #351 base.preview.removeAttribute(attr); } } // build preview container and append preview display $('
') .addClass(kbcss.wrapper) .append(base.$preview) .prependTo(base.$keyboard); } else { base.$preview = base.$el; base.preview = base.el; } }; // Added in v1.26.8 to allow chaining of the caret function, e.g. // keyboard.reveal().caret(4,5).insertText('test').caret('end'); base.caret = function(param1, param2) { var result = $keyboard.caret(base.$preview, param1, param2), wasSetCaret = result instanceof $; // Caret was set, save last position & make chainable if (wasSetCaret) { base.saveCaret(result.start, result.end); return base; } // return caret position if using .caret() return result; }; base.saveCaret = function (start, end, $el) { if (base.isCurrent()) { var p; if (typeof start === 'undefined') { // grab & save current caret position p = $keyboard.caret($el || base.$preview); } else { p = $keyboard.caret($el || base.$preview, start, end); } base.last.start = typeof start === 'undefined' ? p.start : start; base.last.end = typeof end === 'undefined' ? p.end : end; } }; base.saveLastChange = function (val) { base.last.val = val || base.getValue(base.$preview || base.$el); if (base.isContentEditable) { base.last.elms = base.el.cloneNode(true); } }; base.setScroll = function () { // Set scroll so caret & current text is in view // needed for virtual keyboard typing, NOT manual typing - fixes #23 if (!base.isContentEditable && base.last.virtual) { var scrollWidth, clientWidth, adjustment, direction, isTextarea = base.preview.nodeName === 'TEXTAREA', value = base.last.val.substring(0, Math.max(base.last.start, base.last.end)); if (!base.$previewCopy) { // clone preview base.$previewCopy = base.$preview.clone() .removeAttr('id') // fixes #334 .css({ position: 'absolute', left: 0, zIndex: -10, visibility: 'hidden' }) .addClass($keyboard.css.inputClone); // prevent submitting content on form submission base.$previewCopy[0].disabled = true; if (!isTextarea) { // make input zero-width because we need an accurate scrollWidth base.$previewCopy.css({ 'white-space': 'pre', 'width': 0 }); } if (o.usePreview) { // add clone inside of preview wrapper base.$preview.after(base.$previewCopy); } else { // just slap that thing in there somewhere base.$keyboard.prepend(base.$previewCopy); } } if (isTextarea) { // need the textarea scrollHeight, so set the clone textarea height to be the line height base.$previewCopy .height(base.lineHeight) .val(value); // set scrollTop for Textarea base.preview.scrollTop = base.lineHeight * (Math.floor(base.$previewCopy[0].scrollHeight / base.lineHeight) - 1); } else { // add non-breaking spaces base.$previewCopy.val(value.replace(/\s/g, '\xa0')); // if scrollAdjustment option is set to "c" or "center" then center the caret adjustment = /c/i.test(o.scrollAdjustment) ? base.preview.clientWidth / 2 : o.scrollAdjustment; scrollWidth = base.$previewCopy[0].scrollWidth - 1; // set initial state as moving right if (typeof base.last.scrollWidth === 'undefined') { base.last.scrollWidth = scrollWidth; base.last.direction = true; } // if direction = true; we're scrolling to the right direction = base.last.scrollWidth === scrollWidth ? base.last.direction : base.last.scrollWidth < scrollWidth; clientWidth = base.preview.clientWidth - adjustment; // set scrollLeft for inputs; try to mimic the inherit caret positioning + scrolling: // hug right while scrolling right... if (direction) { if (scrollWidth < clientWidth) { base.preview.scrollLeft = 0; } else { base.preview.scrollLeft = scrollWidth - clientWidth; } } else { // hug left while scrolling left... if (scrollWidth >= base.preview.scrollWidth - clientWidth) { base.preview.scrollLeft = base.preview.scrollWidth - adjustment; } else if (scrollWidth - adjustment > 0) { base.preview.scrollLeft = scrollWidth - adjustment; } else { base.preview.scrollLeft = 0; } } base.last.scrollWidth = scrollWidth; base.last.direction = direction; } } }; base.bindFocus = function () { if (o.openOn) { // make sure keyboard isn't destroyed // Check if base exists, this is a case when destroy is called, before timers have fired if (base && base.el.active) { base.$el.bind(o.openOn + base.namespace, function () { base.focusOn(); }); // remove focus from element (needed for IE since blur doesn't seem to work) if ($(':focus')[0] === base.el) { base.$el.blur(); } } } }; base.bindKeyboard = function () { var evt, keyCodes = $keyboard.keyCodes, layout = $keyboard.builtLayouts[base.layout], namespace = base.namespace + 'keybindings'; base.$preview .unbind(base.namespace) .bind('click' + namespace + ' touchstart' + namespace, function () { if (o.alwaysOpen && !base.isCurrent()) { base.reveal(); } // update last caret position after user click, use at least 150ms or it doesn't work in IE base.timer2 = setTimeout(function () { if (base){ base.saveCaret(); } }, 150); }) .bind('keypress' + namespace, function (e) { if (o.lockInput) { return false; } if (!base.isCurrent()) { return; } var k = e.charCode || e.which, // capsLock can only be checked while typing a-z k1 = k >= keyCodes.A && k <= keyCodes.Z, k2 = k >= keyCodes.a && k <= keyCodes.z, str = base.last.key = String.fromCharCode(k); // check, that keypress wasn't rise by functional key // space is first typing symbol in UTF8 table if (k < keyCodes.space) { //see #549 return; } base.last.virtual = false; base.last.event = e; base.last.$key = []; // not a virtual keyboard key if (base.checkCaret) { base.saveCaret(); } // update capsLock if (k !== keyCodes.capsLock && (k1 || k2)) { base.capsLock = (k1 && !e.shiftKey) || (k2 && e.shiftKey); // if shifted keyset not visible, then show it if (base.capsLock && !base.shiftActive) { base.shiftActive = true; base.showSet(); } } // restrict input - keyCode in keypress special keys: // see http://www.asquare.net/javascript/tests/KeyCode.html if (o.restrictInput) { // allow navigation keys to work - Chrome doesn't fire a keypress event (8 = bksp) if ((e.which === keyCodes.backSpace || e.which === 0) && $.inArray(e.keyCode, base.alwaysAllowed)) { return; } // quick key check if ($.inArray(str, layout.acceptedKeys) === -1) { e.preventDefault(); // copy event object in case e.preventDefault() breaks when changing the type evt = $.extend({}, e); evt.type = $keyboard.events.inputRestricted; base.$el.trigger(evt, [base, base.el]); } } else if ((e.ctrlKey || e.metaKey) && (e.which === keyCodes.A || e.which === keyCodes.C || e.which === keyCodes.V || (e.which >= keyCodes.X && e.which <= keyCodes.Z))) { // Allow select all (ctrl-a), copy (ctrl-c), paste (ctrl-v) & cut (ctrl-x) & // redo (ctrl-y)& undo (ctrl-z); meta key for mac return; } // Mapped Keys - allows typing on a regular keyboard and the mapped key is entered // Set up a key in the layout as follows: 'm(a):label'; m = key to map, (a) = actual keyboard key // to map to (optional), ':label' = title/tooltip (optional) // example: \u0391 or \u0391(A) or \u0391:alpha or \u0391(A):alpha if (layout.hasMappedKeys && layout.mappedKeys.hasOwnProperty(str)) { base.last.key = layout.mappedKeys[str]; base.insertText(base.last.key); e.preventDefault(); } if (typeof o.beforeInsert === 'function') { base.insertText(base.last.key); e.preventDefault(); } base.checkMaxLength(); }) .bind('keyup' + namespace, function (e) { if (!base.isCurrent()) { return; } base.last.virtual = false; switch (e.which) { // Insert tab key case keyCodes.tab: // Added a flag to prevent from tabbing into an input, keyboard opening, then adding the tab // to the keyboard preview area on keyup. Sadly it still happens if you don't release the tab // key immediately because keydown event auto-repeats if (base.tab && o.tabNavigation && !o.lockInput) { base.shiftActive = e.shiftKey; // when switching inputs, the tab keyaction returns false var notSwitching = $keyboard.keyaction.tab(base); base.tab = false; if (!notSwitching) { return false; } } else { e.preventDefault(); } break; // Escape will hide the keyboard case keyCodes.escape: if (!o.ignoreEsc) { base.close(o.autoAccept && o.autoAcceptOnEsc ? 'true' : false); } return false; } // throttle the check combo function because fast typers will have an incorrectly positioned caret clearTimeout(base.throttled); base.throttled = setTimeout(function () { // fix error in OSX? see issue #102 if (base && base.isVisible()) { base.checkCombos(); } }, 100); base.checkMaxLength(); base.last.preVal = '' + base.last.val; base.saveLastChange(); // don't alter "e" or the "keyup" event never finishes processing; fixes #552 var event = $.Event( $keyboard.events.kbChange ); // base.last.key may be empty string (shift, enter, tab, etc) when keyboard is first visible // use e.key instead, if browser supports it event.action = base.last.key; base.$el.trigger(event, [base, base.el]); // change callback is no longer bound to the input element as the callback could be // called during an external change event with all the necessary parameters (issue #157) if ($.isFunction(o.change)) { event.type = $keyboard.events.inputChange; o.change(event, base, base.el); return false; } if (o.acceptValid && o.autoAcceptOnValid) { if ( $.isFunction(o.validate) && o.validate(base, base.getValue(base.$preview)) ) { base.$preview.blur(); base.accept(); } } }) .bind('keydown' + namespace, function (e) { base.last.keyPress = e.which; // ensure alwaysOpen keyboards are made active if (o.alwaysOpen && !base.isCurrent()) { base.reveal(); } // prevent tab key from leaving the preview window if (e.which === keyCodes.tab) { // allow tab to pass through - tab to next input/shift-tab for prev base.tab = true; return false; } if (o.lockInput) { return false; } base.last.virtual = false; switch (e.which) { case keyCodes.backSpace: $keyboard.keyaction.bksp(base, null, e); e.preventDefault(); break; case keyCodes.enter: $keyboard.keyaction.enter(base, null, e); break; // Show capsLock case keyCodes.capsLock: base.shiftActive = base.capsLock = !base.capsLock; base.showSet(); break; case keyCodes.V: // prevent ctrl-v/cmd-v if (e.ctrlKey || e.metaKey) { if (o.preventPaste) { e.preventDefault(); return; } base.checkCombos(); // check pasted content } break; } }) .bind('mouseup touchend '.split(' ').join(namespace + ' '), function () { base.last.virtual = true; base.saveCaret(); }); // prevent keyboard event bubbling base.$keyboard.bind('mousedown click touchstart '.split(' ').join(base.namespace + ' '), function (e) { e.stopPropagation(); if (!base.isCurrent()) { base.reveal(); $(base.el.ownerDocument).trigger('checkkeyboard' + base.namespace); } base.setFocus(); }); // If preventing paste, block context menu (right click) if (o.preventPaste) { base.$preview.bind('contextmenu' + base.namespace, function (e) { e.preventDefault(); }); base.$el.bind('contextmenu' + base.namespace, function (e) { e.preventDefault(); }); } }; base.bindKeys = function () { var kbcss = $keyboard.css; base.$allKeys = base.$keyboard.find('button.' + kbcss.keyButton) .unbind(base.namespace + ' ' + base.namespace + 'kb') // Change hover class and tooltip - moved this touchstart before option.keyBinding touchstart // to prevent mousewheel lag/duplication - Fixes #379 & #411 .bind('mouseenter mouseleave touchstart '.split(' ').join(base.namespace + ' '), function (e) { if ((o.alwaysOpen || o.userClosed) && e.type !== 'mouseleave' && !base.isCurrent()) { base.reveal(); base.setFocus(); } if (!base.isCurrent() || this.disabled) { return; } var $keys, txt, last = base.last, $this = $(this), type = e.type; if (o.useWheel && base.wheel) { $keys = base.getLayers($this); txt = ($keys.length ? $keys.map(function () { return $(this).attr('data-value') || ''; }) .get() : '') || [$this.text()]; last.wheel_$Keys = $keys; last.wheelLayers = txt; last.wheelIndex = $.inArray($this.attr('data-value'), txt); } if ((type === 'mouseenter' || type === 'touchstart') && base.el.type !== 'password' && !$this.hasClass(o.css.buttonDisabled)) { $this.addClass(o.css.buttonHover); if (o.useWheel && base.wheel) { $this.attr('title', function (i, t) { // show mouse wheel message return (base.wheel && t === '' && base.sets && txt.length > 1 && type !== 'touchstart') ? o.wheelMessage : t; }); } } if (type === 'mouseleave') { // needed or IE flickers really bad $this.removeClass((base.el.type === 'password') ? '' : o.css.buttonHover); if (o.useWheel && base.wheel) { last.wheelIndex = 0; last.wheelLayers = []; last.wheel_$Keys = null; $this .attr('title', function (i, t) { return (t === o.wheelMessage) ? '' : t; }) .html($this.attr('data-html')); // restore original button text } } }) // keyBinding = 'mousedown touchstart' by default .bind(o.keyBinding.split(' ').join(base.namespace + ' ') + base.namespace + ' ' + $keyboard.events.kbRepeater, function (e) { e.preventDefault(); // prevent errors when external triggers attempt to 'type' - see issue #158 if (!base.$keyboard.is(':visible') || this.disabled) { return false; } var action, $keys, last = base.last, key = this, $key = $(key), // prevent mousedown & touchstart from both firing events at the same time - see #184 timer = new Date().getTime(); if (o.useWheel && base.wheel) { // get keys from other layers/keysets (shift, alt, meta, etc) that line up by data-position $keys = last.wheel_$Keys; // target mousewheel selected key $key = $keys && last.wheelIndex > -1 ? $keys.eq(last.wheelIndex) : $key; } action = $key.attr('data-action'); if (timer - (last.eventTime || 0) < o.preventDoubleEventTime) { return; } last.eventTime = timer; last.event = e; last.virtual = true; last.$key = $key; last.key = $key.attr('data-value'); last.keyPress = ""; // Start caret in IE when not focused (happens with each virtual keyboard button click base.setFocus(); if (/^meta/.test(action)) { action = 'meta'; } // keyaction is added as a string, override original action & text if (action === last.key && typeof $keyboard.keyaction[action] === 'string') { last.key = action = $keyboard.keyaction[action]; } else if (action in $keyboard.keyaction && $.isFunction($keyboard.keyaction[action])) { // stop processing if action returns false (close & cancel) if ($keyboard.keyaction[action](base, this, e) === false) { return false; } action = null; // prevent inserting action name } // stop processing if keyboard closed and keyaction did not return false - see #536 if (!base.hasKeyboard()) { return false; } if (typeof action !== 'undefined' && action !== null) { last.key = $(this).hasClass(kbcss.keyAction) ? action : last.key; base.insertText(last.key); if (!base.capsLock && !o.stickyShift && !e.shiftKey) { base.shiftActive = false; base.showSet($key.attr('data-name')); } } // set caret if caret moved by action function; also, attempt to fix issue #131 $keyboard.caret(base.$preview, last); base.checkCombos(); e.type = $keyboard.events.kbChange; e.action = last.key; base.$el.trigger(e, [base, base.el]); last.preVal = '' + last.val; base.saveLastChange(); if ($.isFunction(o.change)) { e.type = $keyboard.events.inputChange; o.change(e, base, base.el); // return false to prevent reopening keyboard if base.accept() was called return false; } }) // using 'kb' namespace for mouse repeat functionality to keep it separate // I need to trigger a 'repeater.keyboard' to make it work .bind('mouseup' + base.namespace + ' ' + 'mouseleave touchend touchmove touchcancel '.split(' ') .join(base.namespace + 'kb '), function (e) { base.last.virtual = true; var offset, $this = $(this); if (e.type === 'touchmove') { // if moving within the same key, don't stop repeating offset = $this.offset(); offset.right = offset.left + $this.outerWidth(); offset.bottom = offset.top + $this.outerHeight(); if (e.originalEvent.touches[0].pageX >= offset.left && e.originalEvent.touches[0].pageX < offset.right && e.originalEvent.touches[0].pageY >= offset.top && e.originalEvent.touches[0].pageY < offset.bottom) { return true; } } else if (/(mouseleave|touchend|touchcancel)/i.test(e.type)) { $this.removeClass(o.css.buttonHover); // needed for touch devices } else { if (!o.noFocus && base.isCurrent() && base.isVisible()) { base.$preview.focus(); } if (base.checkCaret) { $keyboard.caret(base.$preview, base.last); } } base.mouseRepeat = [false, '']; clearTimeout(base.repeater); // make sure key repeat stops! if (o.acceptValid && o.autoAcceptOnValid) { if ( $.isFunction(o.validate) && o.validate(base, base.getValue()) ) { base.$preview.blur(); base.accept(); } } return false; }) // prevent form submits when keyboard is bound locally - issue #64 .bind('click' + base.namespace, function () { return false; }) // no mouse repeat for action keys (shift, ctrl, alt, meta, etc) .not('.' + kbcss.keyAction) // Allow mousewheel to scroll through other keysets of the same (non-action) key .bind('mousewheel' + base.namespace, function (e, delta) { if (o.useWheel && base.wheel) { // deltaY used by newer versions of mousewheel plugin delta = delta || e.deltaY; var n, txt = base.last.wheelLayers || []; if (txt.length > 1) { n = base.last.wheelIndex + (delta > 0 ? -1 : 1); if (n > txt.length - 1) { n = 0; } if (n < 0) { n = txt.length - 1; } } else { n = 0; } base.last.wheelIndex = n; $(this).html(txt[n]); return false; } }) // mouse repeated action key exceptions .add('.' + kbcss.keyPrefix + ('tab bksp space enter'.split(' ') .join(',.' + kbcss.keyPrefix)), base.$keyboard) .bind('mousedown touchstart '.split(' ').join(base.namespace + 'kb '), function () { if (o.repeatRate !== 0) { var key = $(this); // save the key, make sure we are repeating the right one (fast typers) base.mouseRepeat = [true, key]; setTimeout(function () { // don't repeat keys if it is disabled - see #431 if (base && base.mouseRepeat[0] && base.mouseRepeat[1] === key && !key[0].disabled) { base.repeatKey(key); } }, o.repeatDelay); } return false; }); }; base.execCommand = function(cmd, str) { base.el.ownerDocument.execCommand(cmd, false, str); base.el.normalize(); if (o.reposition) { base.reposition(); } }; base.getValue = function ($el) { $el = $el || base.$preview; return $el[base.isContentEditable ? 'text' : 'val'](); }; base.setValue = function (txt, $el) { $el = $el || base.$preview; if (base.isContentEditable) { if (txt !== $el.text()) { $keyboard.replaceContent($el, txt); base.saveCaret(); } } else { $el.val(txt); } return base; }; // Insert text at caret/selection - thanks to Derek Wickwire for fixing this up! base.insertText = function (txt) { if (!base.$preview) { return base; } if (typeof o.beforeInsert === 'function') { txt = o.beforeInsert(base.last.event, base, base.el, txt); } if (typeof txt === 'undefined' || txt === false) { base.last.key = ''; return base; } if (base.isContentEditable) { return base.insertContentEditable(txt); } var t, bksp = false, isBksp = txt === '\b', // use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE). val = base.getValue(), pos = $keyboard.caret(base.$preview), len = val.length; // save original content length // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea // is still difficult // in IE, pos.end can be zero after input loses focus if (pos.end < pos.start) { pos.end = pos.start; } if (pos.start > len) { pos.end = pos.start = len; } if (base.preview.nodeName === 'TEXTAREA') { // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine) if ($keyboard.msie && val.substr(pos.start, 1) === '\n') { pos.start += 1; pos.end += 1; } } t = pos.start; if (txt === '{d}') { txt = ''; pos.end += 1; } if (isBksp) { txt = ''; bksp = isBksp && t === pos.end && t > 0; } val = val.substr(0, t - (bksp ? 1 : 0)) + txt + val.substr(pos.end); t += bksp ? -1 : txt.length; base.setValue(val); base.saveCaret(t, t); // save caret in case of bksp base.setScroll(); // see #506.. allow chaining of insertText return base; }; base.insertContentEditable = function (txt) { base.$preview.focus(); base.execCommand('insertText', txt); base.saveCaret(); return base; }; // check max length base.checkMaxLength = function () { if (!base.$preview) { return; } var start, caret, val = base.getValue(), len = base.isContentEditable ? $keyboard.getEditableLength(base.el) : val.length; if (o.maxLength !== false && len > o.maxLength) { start = $keyboard.caret(base.$preview).start; caret = Math.min(start, o.maxLength); // prevent inserting new characters when maxed #289 if (!o.maxInsert) { val = base.last.val; caret = start - 1; // move caret back one } base.setValue(val.substring(0, o.maxLength)); // restore caret on change, otherwise it ends up at the end. base.saveCaret(caret, caret); } if (base.$decBtn.length) { base.checkDecimal(); } // allow chaining return base; }; // mousedown repeater base.repeatKey = function (key) { key.trigger($keyboard.events.kbRepeater); if (base.mouseRepeat[0]) { base.repeater = setTimeout(function () { if (base){ base.repeatKey(key); } }, base.repeatTime); } }; base.getKeySet = function () { var sets = []; if (base.altActive) { sets.push('alt'); } if (base.shiftActive) { sets.push('shift'); } if (base.metaActive) { // base.metaActive contains the string name of the // current meta keyset sets.push(base.metaActive); } return sets.length ? sets.join('+') : 'normal'; }; // make it easier to switch keysets via API // showKeySet('shift+alt+meta1') base.showKeySet = function (str) { if (typeof str === 'string') { base.last.keyset = [base.shiftActive, base.altActive, base.metaActive]; base.shiftActive = /shift/i.test(str); base.altActive = /alt/i.test(str); if (/\bmeta/.test(str)) { base.metaActive = true; base.showSet(str.match(/\bmeta[\w-]+/i)[0]); } else { base.metaActive = false; base.showSet(); } } else { base.showSet(str); } // allow chaining return base; }; base.showSet = function (name) { if (!base.hasKeyboard()) { return; } o = base.options; // refresh options var kbcss = $keyboard.css, prefix = '.' + kbcss.keyPrefix, active = o.css.buttonActive, key = '', toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0); if (!base.shiftActive) { base.capsLock = false; } // check meta key set if (base.metaActive) { // remove "-shift" and "-alt" from meta name if it exists if (base.shiftActive) { name = (name || "").replace("-shift", ""); } if (base.altActive) { name = (name || "").replace("-alt", ""); } // the name attribute contains the meta set name 'meta99' key = (/^meta/i.test(name)) ? name : ''; // save active meta keyset name if (key === '') { key = (base.metaActive === true) ? '' : base.metaActive; } else { base.metaActive = key; } // if meta keyset doesn't have a shift or alt keyset, then show just the meta key set if ((!o.stickyShift && base.last.keyset[2] !== base.metaActive) || ((base.shiftActive || base.altActive) && !base.$keyboard.find('.' + kbcss.keySet + '-' + key + base.rows[toShow]).length)) { base.shiftActive = base.altActive = false; } } else if (!o.stickyShift && base.last.keyset[2] !== base.metaActive && base.shiftActive) { // switching from meta key set back to default, reset shift & alt if using stickyShift base.shiftActive = base.altActive = false; } toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0); key = (toShow === 0 && !base.metaActive) ? '-normal' : (key === '') ? '' : '-' + key; if (!base.$keyboard.find('.' + kbcss.keySet + key + base.rows[toShow]).length) { // keyset doesn't exist, so restore last keyset settings base.shiftActive = base.last.keyset[0]; base.altActive = base.last.keyset[1]; base.metaActive = base.last.keyset[2]; return; } base.$keyboard .find(prefix + 'alt,' + prefix + 'shift,.' + kbcss.keyAction + '[class*=meta]') .removeClass(active) .end() .find(prefix + 'alt') .toggleClass(active, base.altActive) .end() .find(prefix + 'shift') .toggleClass(active, base.shiftActive) .end() .find(prefix + 'lock') .toggleClass(active, base.capsLock) .end() .find('.' + kbcss.keySet) .hide() .end() .find('.' + (kbcss.keyAction + prefix + key).replace("--", "-")) .addClass(active); // show keyset using inline-block ( extender layout will then line up ) base.$keyboard.find('.' + kbcss.keySet + key + base.rows[toShow])[0].style.display = 'inline-block'; if (base.metaActive) { base.$keyboard.find(prefix + base.metaActive) // base.metaActive contains the string "meta#" or false // without the !== false, jQuery UI tries to transition the classes .toggleClass(active, base.metaActive !== false); } base.last.keyset = [base.shiftActive, base.altActive, base.metaActive]; base.$el.trigger($keyboard.events.kbKeysetChange, [base, base.el]); if (o.reposition) { base.reposition(); } }; // check for key combos (dead keys) base.checkCombos = function () { // return val for close function if ( !( base.isVisible() || ( base.hasKeyboard() && base.$keyboard.hasClass( $keyboard.css.hasFocus ) ) ) ) { return base.getValue(base.$preview || base.$el); } var r, t, t2, repl, // use base.$preview.val() instead of base.preview.value // (val.length includes carriage returns in IE). val = base.getValue(), pos = $keyboard.caret(base.$preview), layout = $keyboard.builtLayouts[base.layout], max = base.isContentEditable ? $keyboard.getEditableLength(base.el) : val.length, // save original content length len = max; // return if val is empty; fixes #352 if (val === '') { // check valid on empty string - see #429 if (o.acceptValid) { base.checkValid(); } return val; } // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea // is still difficult // in IE, pos.end can be zero after input loses focus if (pos.end < pos.start) { pos.end = pos.start; } if (pos.start > len) { pos.end = pos.start = len; } // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine) if ($keyboard.msie && val.substr(pos.start, 1) === '\n') { pos.start += 1; pos.end += 1; } if (o.useCombos) { // keep 'a' and 'o' in the regex for ae and oe ligature (æ,œ) // thanks to KennyTM: http://stackoverflow.com/q/4275077 // original regex /([`\'~\^\"ao])([a-z])/mig moved to $.keyboard.comboRegex if ($keyboard.msie) { // old IE may not have the caret positioned correctly, so just check the whole thing val = val.replace(base.regex, function (s, accent, letter) { return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s; }); // prevent combo replace error, in case the keyboard closes - see issue #116 } else if (base.$preview.length) { // Modern browsers - check for combos from last two characters left of the caret t = pos.start - (pos.start - 2 >= 0 ? 2 : 0); // target last two characters $keyboard.caret(base.$preview, t, pos.end); // do combo replace t = $keyboard.caret(base.$preview); repl = function (txt) { return (txt || '').replace(base.regex, function (s, accent, letter) { return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s; }); }; t2 = repl(t.text); // add combo back // prevent error if caret doesn't return a function if (t && t.replaceStr && t2 !== t.text) { if (base.isContentEditable) { $keyboard.replaceContent(el, repl); } else { base.setValue(t.replaceStr(t2)); } } val = base.getValue(); } } // check input restrictions - in case content was pasted if (o.restrictInput && val !== '') { t = layout.acceptedKeys.length; r = layout.acceptedKeysRegex; if (!r) { t2 = $.map(layout.acceptedKeys, function (v) { // escape any special characters return v.replace(base.escapeRegex, '\\$&'); }); r = layout.acceptedKeysRegex = new RegExp('(' + t2.join('|') + ')', 'g'); } // only save matching keys t2 = val.match(r); if (t2) { val = t2.join(''); } else { // no valid characters val = ''; len = 0; } } // save changes, then reposition caret pos.start += max - len; pos.end += max - len; base.setValue(val); base.saveCaret(pos.start, pos.end); // set scroll to keep caret in view base.setScroll(); base.checkMaxLength(); if (o.acceptValid) { base.checkValid(); } return val; // return text, used for keyboard closing section }; // Toggle accept button classes, if validating base.checkValid = function () { var kbcss = $keyboard.css, $accept = base.$keyboard.find('.' + kbcss.keyPrefix + 'accept'), valid = true; if ($.isFunction(o.validate)) { valid = o.validate(base, base.getValue(), false); } // toggle accept button classes; defined in the css $accept .toggleClass(kbcss.inputInvalid, !valid) .toggleClass(kbcss.inputValid, valid) // update title to indicate that the entry is valid or invalid .attr('title', $accept.attr('data-title') + ' (' + o.display[valid ? 'valid' : 'invalid'] + ')'); }; // Decimal button for num pad - only allow one (not used by default) base.checkDecimal = function () { // Check US '.' or European ',' format if ((base.decimal && /\./g.test(base.preview.value)) || (!base.decimal && /\,/g.test(base.preview.value))) { base.$decBtn .attr({ 'disabled': 'disabled', 'aria-disabled': 'true' }) .removeClass(o.css.buttonHover) .addClass(o.css.buttonDisabled); } else { base.$decBtn .removeAttr('disabled') .attr({ 'aria-disabled': 'false' }) .addClass(o.css.buttonDefault) .removeClass(o.css.buttonDisabled); } }; // get other layer values for a specific key base.getLayers = function ($el) { var kbcss = $keyboard.css, key = $el.attr('data-pos'), $keys = $el.closest('.' + kbcss.keyboard) .find('button[data-pos="' + key + '"]'); return $keys.filter(function () { return $(this) .find('.' + kbcss.keyText) .text() !== ''; }) .add($el); }; // Go to next or prev inputs // goToNext = true, then go to next input; if false go to prev // isAccepted is from autoAccept option or true if user presses shift+enter base.switchInput = function (goToNext, isAccepted) { if ($.isFunction(o.switchInput)) { o.switchInput(base, goToNext, isAccepted); } else { // base.$keyboard may be an empty array - see #275 (apod42) if (base.$keyboard.length) { base.$keyboard.hide(); } var kb, stopped = false, all = $('button, input, select, textarea, a, [contenteditable]') .filter(':visible') .not(':disabled'), indx = all.index(base.$el) + (goToNext ? 1 : -1); if (base.$keyboard.length) { base.$keyboard.show(); } if (indx > all.length - 1) { stopped = o.stopAtEnd; indx = 0; // go to first input } if (indx < 0) { stopped = o.stopAtEnd; indx = all.length - 1; // stop or go to last } if (!stopped) { isAccepted = base.close(isAccepted); if (!isAccepted) { return; } kb = all.eq(indx).data('keyboard'); if (kb && kb.options.openOn.length) { kb.focusOn(); } else { all.eq(indx).focus(); } } } return false; }; // Close the keyboard, if visible. Pass a status of true, if the content was accepted // (for the event trigger). base.close = function (accepted) { if (base.isOpen && base.$keyboard.length) { clearTimeout(base.throttled); var kbcss = $keyboard.css, kbevents = $keyboard.events, val = accepted ? base.checkCombos() : base.originalContent; // validate input if accepted if (accepted && $.isFunction(o.validate) && !o.validate(base, val, true)) { val = base.originalContent; accepted = false; if (o.cancelClose) { return; } } base.isCurrent(false); base.isOpen = o.alwaysOpen || o.userClosed; if (base.isContentEditable && !accepted) { // base.originalContent stores the HTML base.$el.html(val); } else { base.setValue(val, base.$el); } base.$el .removeClass(kbcss.isCurrent + ' ' + kbcss.inputAutoAccepted) // add 'ui-keyboard-autoaccepted' to inputs - see issue #66 .addClass((accepted || false) ? accepted === true ? '' : kbcss.inputAutoAccepted : '') // trigger default change event - see issue #146 .trigger(kbevents.inputChange); // don't trigger an empty event - see issue #463 if (!o.alwaysOpen) { // don't trigger beforeClose if keyboard is always open base.$el.trigger(kbevents.kbBeforeClose, [base, base.el, (accepted || false)]); } // save caret after updating value (fixes userClosed issue with changing focus) $keyboard.caret(base.$preview, base.last); base.$el .trigger(((accepted || false) ? kbevents.inputAccepted : kbevents.inputCanceled), [base, base.el]) .trigger((o.alwaysOpen) ? kbevents.kbInactive : kbevents.kbHidden, [base, base.el]) .blur(); // base is undefined if keyboard was destroyed - fixes #358 if (base) { // add close event time base.last.eventTime = new Date().getTime(); if (!(o.alwaysOpen || o.userClosed && accepted === 'true') && base.$keyboard.length) { // free up memory base.removeKeyboard(); // rebind input focus - delayed to fix IE issue #72 base.timer = setTimeout(function () { if (base) { base.bindFocus(); } }, 500); } if (!base.watermark && base.el.value === '' && base.inPlaceholder !== '') { base.$el.addClass(kbcss.placeholder); base.setValue(base.inPlaceholder, base.$el); } } } return !!accepted; }; base.accept = function () { return base.close(true); }; base.checkClose = function (e) { if (base.opening) { return; } var kbcss = $.keyboard.css, name = e.target.nodeName, $target = name === 'INPUT' || name === 'TEXTAREA' ? $(e.target) : // clicking on an element inside of a contenteditable $(e.target).closest('[contenteditable]'); base.escClose(e, $target); // needed for IE to allow switching between keyboards smoothly if ($target.hasClass(kbcss.input)) { var kb = $target.data('keyboard'); // only trigger on self if ( kb !== base && !kb.$el.hasClass(kbcss.isCurrent) && kb.options.openOn && e.type === o.openOn ) { kb.focusOn(); } } }; // callback functions called to check if the keyboard needs to be closed // e.g. on escape or clicking outside the keyboard base.escCloseCallback = { // keep keyboard open if alwaysOpen or stayOpen is true - fixes mutliple // always open keyboards or single stay open keyboard keepOpen: function($target) { return !base.isOpen; } }; base.escClose = function (e, $el) { if (e && e.type === 'keyup') { return (e.which === $keyboard.keyCodes.escape && !o.ignoreEsc) ? base.close(o.autoAccept && o.autoAcceptOnEsc ? 'true' : false) : ''; } var shouldStayOpen = false, $target = $el || $(e.target); $.each(base.escCloseCallback, function(i, callback) { if (typeof callback === 'function') { shouldStayOpen = shouldStayOpen || callback($target); } }); if (shouldStayOpen) { return; } // ignore autoaccept if using escape - good idea? if (!base.isCurrent() && base.isOpen || base.isOpen && $target[0] !== base.el) { // don't close if stayOpen is set; but close if a different keyboard is being opened if ((o.stayOpen || o.userClosed) && !$target.hasClass($keyboard.css.input)) { return; } // stop propogation in IE - an input getting focus doesn't open a keyboard if one is already open if ($keyboard.allie) { e.preventDefault(); } if (o.closeByClickEvent) { // only close the keyboard if the user is clicking on an input or if they cause a click // event (touchstart/mousedown will not force the close with this setting) var name = $target[0] && $target[0].nodeName.toLowerCase(); if (name === 'input' || name === 'textarea' || e.type === 'click') { base.close(o.autoAccept ? 'true' : false); } } else { // send 'true' instead of a true (boolean), the input won't get a 'ui-keyboard-autoaccepted' // class name - see issue #66 base.close(o.autoAccept ? 'true' : false); } } }; // Build default button base.keyBtn = $('') .attr({ 'role': 'button', 'type': 'button', 'aria-disabled': 'false', 'tabindex': '-1' }) .addClass($keyboard.css.keyButton); // convert key names into a class name base.processName = function (name) { var index, n, process = (name || '').replace(/[^a-z0-9-_]/gi, ''), len = process.length, newName = []; if (len > 1 && name === process) { // return name if basic text return name; } // return character code sequence len = name.length; if (len) { for (index = 0; index < len; index++) { n = name[index]; // keep '-' and '_'... so for dash, we get two dashes in a row newName.push(/[a-z0-9-_]/i.test(n) ? (/[-_]/.test(n) && index !== 0 ? '' : n) : (index === 0 ? '' : '-') + n.charCodeAt(0) ); } return newName.join(''); } else { return name; } }; base.processKeys = function (name) { var tmp, parts = name.split(':'), data = { name: null, map: '', title: '' }; /* map defined keys format 'key(A):Label_for_key_(ignore_parentheses_here)' 'key' = key that is seen (can any character(s); but it might need to be escaped using '\' or entered as unicode '\u####' '(A)' = the actual key on the real keyboard to remap ':Label_for_key' ends up in the title/tooltip Examples: '\u0391(A):alpha', 'x(y):this_(might)_cause_problems or edge cases of ':(x)', 'x(:)', 'x(()' or 'x())' Enhancement (if I can get alt keys to work): A mapped key will include the mod key, e.g. 'x(alt-x)' or 'x(alt-shift-x)' */ if (/\(.+\)/.test(parts[0]) || /^:\(.+\)/.test(name) || /\([(:)]\)/.test(name)) { // edge cases 'x(:)', 'x(()' or 'x())' if (/\([(:)]\)/.test(name)) { tmp = parts[0].match(/([^(]+)\((.+)\)/); if (tmp && tmp.length) { data.name = tmp[1]; data.map = tmp[2]; data.title = parts.length > 1 ? parts.slice(1).join(':') : ''; } else { // edge cases 'x(:)', ':(x)' or ':(:)' data.name = name.match(/([^(]+)/)[0]; if (data.name === ':') { // ':(:):test' => parts = [ '', '(', ')', 'title' ] need to slice 1 parts = parts.slice(1); } if (tmp === null) { // 'x(:):test' => parts = [ 'x(', ')', 'title' ] need to slice 2 data.map = ':'; parts = parts.slice(2); } data.title = parts.length ? parts.join(':') : ''; } } else { // example: \u0391(A):alpha; extract 'A' from '(A)' data.map = name.match(/\(([^()]+?)\)/)[1]; // remove '(A)', left with '\u0391:alpha' name = name.replace(/\(([^()]+)\)/, ''); tmp = name.split(':'); // get '\u0391' from '\u0391:alpha' if (tmp[0] === '') { data.name = ':'; parts = parts.slice(1); } else { data.name = tmp[0]; } data.title = parts.length > 1 ? parts.slice(1).join(':') : ''; } } else { // find key label // corner case of '::;' reduced to ':;', split as ['', ';'] if (name !== '' && parts[0] === '') { data.name = ':'; parts = parts.slice(1); } else { data.name = parts[0]; } data.title = parts.length > 1 ? parts.slice(1).join(':') : ''; } data.title = $.trim(data.title).replace(/_/g, ' '); return data; }; // Add key function // keyName = the name of the function called in $.keyboard.keyaction when the button is clicked // name = name added to key, or cross-referenced in the display options // base.temp[0] = keyset to attach the new button // regKey = true when it is not an action key base.addKey = function (keyName, action, regKey) { var keyClass, tmp, keys, data = {}, txt = base.processKeys(regKey ? keyName : action), kbcss = $keyboard.css; if (!regKey && o.display[txt.name]) { keys = base.processKeys(o.display[txt.name]); // action contained in "keyName" (e.g. keyName = "accept", // action = "a" (use checkmark instead of text)) keys.action = base.processKeys(keyName).name; } else { // when regKey is true, keyName is the same as action keys = txt; keys.action = txt.name; } data.name = base.processName(txt.name); if (keys.name !== '') { if (keys.map !== '') { $keyboard.builtLayouts[base.layout].mappedKeys[keys.map] = keys.name; $keyboard.builtLayouts[base.layout].acceptedKeys.push(keys.name); } else if (regKey) { $keyboard.builtLayouts[base.layout].acceptedKeys.push(keys.name); } } if (regKey) { keyClass = data.name === '' ? '' : kbcss.keyPrefix + data.name; } else { // Action keys will have the 'ui-keyboard-actionkey' class keyClass = kbcss.keyAction + ' ' + kbcss.keyPrefix + keys.action; } // '\u2190'.length = 1 because the unicode is converted, so if more than one character, // add the wide class keyClass += (keys.name.length > 2 ? ' ' + kbcss.keyWide : '') + ' ' + o.css.buttonDefault; data.html = '' + // this prevents HTML from being added to the key keys.name.replace(/[\u00A0-\u9999]/gim, function (i) { return '' + i.charCodeAt(0) + ';'; }) + ''; data.$key = base.keyBtn .clone() .attr({ 'data-value': regKey ? keys.name : keys.action, // value 'data-name': keys.action, 'data-pos': base.temp[1] + ',' + base.temp[2], 'data-action': keys.action, 'data-html': data.html }) // add 'ui-keyboard-' + data.name for all keys // (e.g. 'Bksp' will have 'ui-keyboard-bskp' class) // any non-alphanumeric characters will be replaced with // their decimal unicode value // (e.g. '~' is a regular key, class = 'ui-keyboard-126' // (126 is the unicode decimal value - same as ~) // See https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes .addClass(keyClass) .html(data.html) .appendTo(base.temp[0]); if (keys.map) { data.$key.attr('data-mapped', keys.map); } if (keys.title || txt.title) { data.$key.attr({ 'data-title': txt.title || keys.title, // used to allow adding content to title 'title': txt.title || keys.title }); } if (typeof o.buildKey === 'function') { data = o.buildKey(base, data); // copy html back to attributes tmp = data.$key.html(); data.$key.attr('data-html', tmp); } return data.$key; }; base.customHash = function (layout) { /*jshint bitwise:false */ var i, array, hash, character, len, arrays = [], merged = []; // pass layout to allow for testing layout = typeof layout === 'undefined' ? o.customLayout : layout; // get all layout arrays for (array in layout) { if (layout.hasOwnProperty(array)) { arrays.push(layout[array]); } } // flatten array merged = merged.concat.apply(merged, arrays).join(' '); // produce hash name - http://stackoverflow.com/a/7616484/145346 hash = 0; len = merged.length; if (len === 0) { return hash; } for (i = 0; i < len; i++) { character = merged.charCodeAt(i); hash = ((hash << 5) - hash) + character; hash = hash & hash; // Convert to 32bit integer } return hash; }; base.buildKeyboard = function (name, internal) { // o.display is empty when this is called from the scramble extension (when alwaysOpen:true) if ($.isEmptyObject(o.display)) { // set keyboard language base.updateLanguage(); } var index, row, $row, currentSet, kbcss = $keyboard.css, sets = 0, layout = $keyboard.builtLayouts[name || base.layout || o.layout] = { mappedKeys: {}, acceptedKeys: [] }, acceptedKeys = layout.acceptedKeys = o.restrictInclude ? ('' + o.restrictInclude).split(/\s+/) || [] : [], // using $layout temporarily to hold keyboard popup classnames $layout = kbcss.keyboard + ' ' + o.css.popup + ' ' + o.css.container + (o.alwaysOpen || o.userClosed ? ' ' + kbcss.alwaysOpen : ''), container = $('') .addClass($layout) .attr({ 'role': 'textbox' }) .hide(); // allow adding "{space}" as an accepted key - Fixes #627 index = $.inArray('{space}', acceptedKeys); if (index > -1) { acceptedKeys[index] = ' '; } // verify layout or setup custom keyboard if ((internal && o.layout === 'custom') || !$keyboard.layouts.hasOwnProperty(o.layout)) { o.layout = 'custom'; $layout = $keyboard.layouts.custom = o.customLayout || { 'normal': ['{cancel}'] }; } else { $layout = $keyboard.layouts[internal ? o.layout : name || base.layout || o.layout]; } // Main keyboard building loop $.each($layout, function (set, keySet) { // skip layout name & lang settings if (set !== '' && !/^(name|lang|rtl)$/i.test(set)) { // keep backwards compatibility for change from default to normal naming if (set === 'default') { set = 'normal'; } sets++; $row = $('') .attr('name', set) // added for typing extension .addClass(kbcss.keySet + ' ' + kbcss.keySet + '-' + set) .appendTo(container) .toggle(set === 'normal'); for (row = 0; row < keySet.length; row++) { // remove extra spaces before spliting (regex probably could be improved) currentSet = $.trim(keySet[row]).replace(/\{(\.?)[\s+]?:[\s+]?(\.?)\}/g, '{$1:$2}'); base.buildRow($row, row, currentSet.split(/\s+/), acceptedKeys); $row.find('.' + kbcss.keyButton + ',.' + kbcss.keySpacer) .filter(':last') .after('