/*! jQuery UI Virtual Keyboard v1.25.6 *//* Author: Jeremy Satterfield Modified: Rob Garrison (Mottie on github) ----------------------------------------- Licensed under the MIT License Caret code modified from jquery.caret.1.02.js Licensed under the MIT License: http://www.opensource.org/licenses/mit-license.php ----------------------------------------- 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. As a plugin to jQuery UI styling and theme will automatically match that used by jQuery UI with the exception of the required CSS. Requires: jQuery v1.4.3+ Optional: jQuery UI (position utility only) & CSS theme jQuery mousewheel Setup/Usage: Please refer to https://github.com/Mottie/Keyboard/wiki */ /*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 base = this, o; base.version = '1.25.6'; // Access to jQuery and DOM versions of element base.$el = $(el); base.el = el; // Add a reverse reference to the DOM object base.$el.data('keyboard', base); base.init = function(){ var position, kbcss = $keyboard.css, close; 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 ); // keyCode of keys always allowed to be typed - caps lock, page up & down, end, home, arrow, insert & // delete keys base.alwaysAllowed = [20,33,34,35,36,37,38,39,40,45,46]; base.$keyboard = []; // keyboard enabled base.enabled = true; // make a copy of the original keyboard position if (!$.isEmptyObject(o.position)) { o.position.orig_at = o.position.at; } base.checkCaret = ( o.lockInput || $keyboard.checkCaretSupport() ); // [shift, alt, meta] base.last = { start: 0, end: 0, key: '', val: '', layout: '', virtual: true, keyset: [ false, false, false ], wheel_$Keys : null, wheelIndex : 0, wheelLayers: [] }; base.temp = [ '', 0, 0 ]; // used when building the keyboard - [keyset element, row, index] // Bind events $.each('initialized beforeVisible visible hidden canceled accepted beforeClose'.split(' '), function( i, f ) { if ($.isFunction(o[f])){ base.$el.bind(f + base.namespace, o[f]); } }); // Close with esc key & clicking outside if (o.alwaysOpen) { o.stayOpen = true; } close = function( e ) { if (base.opening) { return; } base.escClose(e); var $target = $(e.target); // needed for IE to allow switching between keyboards smoothly if ( $target.hasClass( kbcss.input ) ) { var kb = $target.data('keyboard'); // only trigger on self if ( kb === base && !kb.$el.hasClass( kbcss.isCurrent ) && e.type === kb.options.openOn ) { kb.focusOn(); } } }; $(document).bind('mousedown keyup touchstart checkkeyboard '.split(' ').join(base.namespace + ' '),close); if (base.el.ownerDocument !== document) { $(base.el.ownerDocument).bind('mousedown keyup touchstart checkkeyboard '.split(' ').join(base.namespace + ' '),close); } // Display keyboard on focus base.$el .addClass( kbcss.input + ' ' + o.css.input ) .attr({ 'aria-haspopup' : 'true', 'role' : 'textbox' }); // 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.$el.bind(o.openOn + base.namespace, function(){ base.focusOn(); }); } // Add placeholder if not supported by the browser if (!base.watermark && base.$el.val() === '' && base.inPlaceholder !== '' && base.$el.attr('placeholder') !== '') { base.$el .addClass(kbcss.placeholder) // css watermark style (darker text) .val( base.inPlaceholder ); } base.$el.trigger( $keyboard.events.kbInit, [ base, base.el ] ); // initialized with keyboard open if (o.alwaysOpen) { base.reveal(); } }; base.toggle = function(){ var $toggle = base.$keyboard.find( '.' + $keyboard.css.keyToggle ), locked = !base.enabled; // prevent physical keyboard from working base.$preview.prop( 'readonly', locked || base.options.lockInput ); // disable all buttons base.$keyboard .toggleClass( $keyboard.css.keyDisabled, locked ) .find( '.' + $keyboard.css.keyButton ) .not( $toggle ) .prop( 'disabled', locked ) .attr( 'aria-disabled', locked ); $toggle.toggleClass( $keyboard.css.keyDisabled, locked ); // stop auto typing if ( locked && base.typing_options ) { base.typing_options.text = ''; } }; base.setCurrent = function(){ var kbcss = $keyboard.css; // ui-keyboard-has-focus is applied in case multiple keyboards have alwaysOpen = true and are stacked $('.' + kbcss.hasFocus).removeClass(kbcss.hasFocus); $('.' + kbcss.isCurrent).removeClass(kbcss.isCurrent); base.$el.addClass(kbcss.isCurrent); base.$keyboard.addClass(kbcss.hasFocus); base.isCurrent(true); base.isOpen = true; }; base.isCurrent = function(set){ var cur = $keyboard.currentKeyboard || false; if (set) { cur = $keyboard.currentKeyboard = base.el; } else if (set === false && cur === base.el) { cur = $keyboard.currentKeyboard = ''; } return cur === base.el; }; base.isVisible = function() { return base.$keyboard && base.$keyboard.length ? base.$keyboard.is(':visible') : false; }; base.focusOn = function(){ if ( !base && base.el.active ) { // keyboard was destroyed return; } if (base.$el.is(':visible')) { // 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 // add delay to get correct caret position base.timer2 = setTimeout(function(){ var undef; // 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 ) { base.saveCaret( undef, undef, base.$el ); } }, 20); } if (!base.isVisible()) { clearTimeout(base.timer); base.reveal(); } if (o.alwaysOpen) { base.setCurrent(); } }; base.reveal = function(refresh){ if (base.isOpen) { refresh = true; } var kbcss = $keyboard.css; base.opening = true; // remove all 'extra' keyboards $('.' + kbcss.keyboard).not('.' + kbcss.alwaysOpen).remove(); // update keyboard after a layout change if (refresh) { base.isOpen = false; base.last.val = base.$preview && base.$preview.val() || ''; if (base.$keyboard.length) { base.$keyboard.remove(); base.$keyboard = []; base.shiftActive = base.altActive = base.metaActive = 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( o.openOn + base.namespace ); } // build keyboard if it doesn't exist; or attach keyboard if it was removed, but not cleared if ( !base.$keyboard || base.$keyboard && ( !base.$keyboard.length || $.contains(document.body, base.$keyboard[0]) ) ) { base.startup(); } // clear watermark if (!base.watermark && base.el.value === base.inPlaceholder) { base.$el .removeClass(kbcss.placeholder) .val(''); } // save starting content, in case we cancel base.originalContent = base.$el.val(); base.$preview.val( refresh ? base.last.val : base.originalContent ); // disable/enable accept button if (o.acceptValid) { base.checkValid(); } if (o.resetDefault) { base.shiftActive = base.altActive = base.metaActive = false; } base.showSet(); // beforeVisible event base.$el.trigger( $keyboard.events.kbBeforeVisible, [ base, base.el ] ); base.setCurrent(); // update keyboard - enabled or disabled? base.toggle(); // show keyboard base.$keyboard.show(); // adjust keyboard preview window width - save width so IE won't keep expanding (fix issue #6) if (o.usePreview && $keyboard.msie) { if (typeof base.width === 'undefined') { base.$preview.hide(); // preview is 100% browser width in IE7, so hide the damn thing base.width = Math.ceil(base.$keyboard.width()); // set input width to match the widest keyboard row base.$preview.show(); } base.$preview.width(base.width); } base.position = $.isEmptyObject(o.position) ? false : o.position; // position after keyboard is visible (required for UI position utility) and appropriately sized if ($.ui && $.ui.position && base.position) { // get single target position || target stored in element data (multiple targets) || default @ element base.position.of = base.position.of || base.$el.data('keyboardPosition') || base.$el; base.position.collision = base.position.collision || 'flipfit flipfit'; o.position.at = o.usePreview ? o.position.orig_at : o.position.at2; base.$keyboard.position(base.position); } base.checkDecimal(); // get preview area line height // add roughly 4px to get line height from font height, works well for font-sizes from 14-36px // needed for textareas base.lineHeight = parseInt( base.$preview.css('lineHeight'), 10) || parseInt(base.$preview.css('font-size') ,10) + 4; if (o.caretToEnd) { base.saveCaret( base.originalContent.length, base.originalContent.length ); } // IE caret haxx0rs if ($keyboard.allie){ // sometimes end = 0 while start is > 0 if (base.last.end === 0 && base.last.start > 0) { base.last.end = base.last.start; } // IE will have start -1, end of 0 when not focused (see demo: http://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; } } // opening keyboard flag; delay allows switching between keyboards without immediately closing // the keyboard base.timer2 = setTimeout(function(){ base.opening = false; if (o.initialFocus) { $keyboard.caret( base.$preview, base.last ); } // save event time for keyboards with stayOpen: true base.last.eventTime = new Date().getTime(); base.$el.trigger( $keyboard.events.kbVisible, [ base, base.el ] ); base.timer = setTimeout(function(){ // get updated caret information after visible event - fixes #331 if (base) { // Check if base exists, this is a case when destroy is called, before timers have fired base.saveCaret(); } }, 200); }, 10); // return base to allow chaining in typing extension return base; }; base.updateLanguage = function(){ // change language if layout is named something like 'french-azerty-1' var layouts = $keyboard.layouts, lang = o.language || layouts[ o.layout ] && layouts[ o.layout ].lang && layouts[ o.layout ].lang || [ o.language || 'en' ], kblang = $keyboard.language; // some languages include a dash, e.g. 'en-gb' or 'fr-ca' // allow o.language to be a string or array... // array is for future expansion where a layout can be set for multiple languages lang = ( $.isArray(lang) ? lang[0] : lang ).split('-')[0]; // set keyboard language o.display = $.extend( true, {}, kblang.en.display, kblang[ lang ] && kblang[ lang ].display || {}, base.settings.display ); o.combos = $.extend( true, {}, kblang.en.combos, kblang[ lang ] && kblang[ lang ].combos || {}, base.settings.combos ); o.wheelMessage = kblang[ lang ] && kblang[ lang ].wheelMessage || kblang.en.wheelMessage; // rtl can be in the layout or in the language definition; defaults to false o.rtl = layouts[ o.layout ] && layouts[ o.layout ].rtl || kblang[ lang ] && kblang[ lang ].rtl || false; // save default regex (in case loading another layout changes it) base.regex = kblang[ lang ] && kblang[ lang ].comboRegex || $keyboard.comboRegex; // determine if US '.' or European ',' system being used base.decimal = /^\./.test(o.display.dec); base.$el .toggleClass('rtl', o.rtl) .css('direction', o.rtl ? 'rtl' : ''); }; base.startup = function(){ var kbcss = $keyboard.css; // ensure base.$preview is defined base.$preview = base.$el; if ( !(base.$keyboard && base.$keyboard.length) ) { // custom layout - create a unique layout name based on the hash if (o.layout === 'custom') { o.layoutHash = 'custom' + base.customHash(); } base.layout = o.layout === 'custom' ? o.layoutHash : o.layout; base.last.layout = base.layout; base.updateLanguage(); if (typeof $keyboard.builtLayouts[base.layout] === 'undefined') { if ($.isFunction(o.create)) { // create must call buildKeyboard() function; or create it's own keyboard base.$keyboard = o.create(base); } else if (!base.$keyboard.length) { base.buildKeyboard(base.layout, true); } } base.$keyboard = $keyboard.builtLayouts[base.layout].$keyboard.clone(); base.$keyboard.data( 'keyboard', base ); if ( ( base.el.id || '' ) !== '' ) { // add ID to keyboard for styling purposes base.$keyboard.attr( 'id', base.el.id + $keyboard.css.idSuffix ); } // build preview display if (o.usePreview) { // restore original positioning (in case usePreview option is altered) if (!$.isEmptyObject(o.position)) { o.position.at = o.position.orig_at; } base.$preview = base.$el.clone(false) .removeAttr('id') .data( 'keyboard', base ) .removeClass(kbcss.placeholder + ' ' + kbcss.input) .addClass(kbcss.preview + ' ' + o.css.input) .removeAttr('aria-haspopup') .attr('tabindex', '-1') .show(); // for hidden inputs // Switch the number input fields to text so the caret positioning will work again if (base.$preview.attr('type') == 'number') { base.$preview.attr('type', 'text'); } // build preview container and append preview display $('
') .addClass(kbcss.wrapper) .append(base.$preview) .prependTo(base.$keyboard); } else { // No preview display, use element and reposition the keyboard under it. if (!$.isEmptyObject(o.position)) { o.position.at = o.position.at2; } } } base.preview = base.$preview[0]; 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); } if (o.lockInput) { base.$preview.addClass(kbcss.locked).attr({ 'readonly': 'readonly'}); } base.bindKeyboard(); base.$keyboard.appendTo( o.appendLocally ? base.$el.parent() : o.appendTo || 'body' ); base.bindKeys(); // adjust with window resize; don't check base.position // here in case it is changed dynamically if (o.reposition && $.ui && $.ui.position && o.appendTo == 'body') { $(window).bind('resize' + base.namespace, function(){ if (base.position && base.isVisible()) { base.$keyboard.position(base.position); } }); } }; base.saveCaret = function(start, end, $el){ var p = $keyboard.caret( $el || base.$preview, start, end ); base.last.start = typeof start === 'undefined' ? p.start : start; base.last.end = typeof end === 'undefined' ? p.end : end; }; base.setScroll = function(){ // Set scroll so caret & current text is in view // needed for virtual keyboard typing, NOT manual typing - fixes #23 if ( base.last.virtual ) { var scrollWidth, clientWidth, adjustment, direction, isTextarea = base.preview.nodeName === 'TEXTAREA', value = base.last.val.substring( 0, Math.max( base.last.start, base.last.end ) ); if ( !base.$previewCopy ) { // clone preview base.$previewCopy = base.$preview.clone() .removeAttr('id') // fixes #334 .css({ position : 'absolute', left: 0, zIndex : -10, visibility : 'hidden' }) .addClass('ui-keyboard-preview-clone'); 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.bindKeyboard = function(){ var evt, layout = $keyboard.builtLayouts[base.layout]; base.$preview .unbind('keypress keyup keydown mouseup touchend '.split(' ').join(base.namespace + ' ')) .bind('click' + base.namespace, function(){ // update last caret position after user click, use at least 150ms or it doesn't work in IE base.timer2 = setTimeout(function(){ base.saveCaret(); }, 150); }) .bind('keypress' + base.namespace, function(e){ if (o.lockInput) { return false; } var k = e.charCode || e.which, str = base.last.key = String.fromCharCode(k); base.last.virtual = false; base.last.event = e; base.last.$key = []; // not a virtual keyboard key if (base.checkCaret) { base.saveCaret(); } // update caps lock - can only do this while typing =( base.capsLock = (((k >= 65 && k <= 90) && !e.shiftKey) || ((k >= 97 && k <= 122) && e.shiftKey)) ? true : false; // 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 === 8 || e.which === 0) && $.inArray( e.keyCode, base.alwaysAllowed ) ) { return; } // quick key check if ($.inArray(k, layout.acceptedKeys) === -1) { e.preventDefault(); // copy event object in case e.preventDefault() breaks when changing the type evt = $.extend({}, e); evt.type = $keyboard.events.inputRestricted; base.$el.trigger( evt, [ base, base.el ] ); if ( $.isFunction(o.restricted) ) { o.restricted( evt, base, base.el ); } } } else if ( (e.ctrlKey || e.metaKey) && (e.which === 97 || e.which === 99 || e.which === 118 || (e.which >= 120 && e.which <=122)) ) { // Allow select all (ctrl-a:97), copy (ctrl-c:99), paste (ctrl-v:118) & cut (ctrl-x:120) & // redo (ctrl-y:121)& undo (ctrl-z:122); meta key for mac return; } // Mapped Keys - allows typing on a regular keyboard and the mapped key is entered // Set up a key in the layout as follows: 'm(a):label'; m = key to map, (a) = actual keyboard key // to map to (optional), ':label' = title/tooltip (optional) // example: \u0391 or \u0391(A) or \u0391:alpha or \u0391(A):alpha if (layout.hasMappedKeys && layout.mappedKeys.hasOwnProperty(str)){ base.last.key = layout.mappedKeys[str]; base.insertText( base.last.key ); e.preventDefault(); } base.checkMaxLength(); }) .bind('keyup' + base.namespace, function(e){ base.last.virtual = false; switch (e.which) { // Insert tab key case 9 : // 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 27: 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.isVisible()) { base.checkCombos(); } }, 100); base.checkMaxLength(); // 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) base.$el.trigger( $keyboard.events.kbChange, [ base, base.el ] ); base.last.val = base.$preview.val(); if ($.isFunction(o.change)){ o.change( $.Event( $keyboard.events.inputChange ), base, base.el ); return false; } }) .bind('keydown' + base.namespace, function(e){ // prevent tab key from leaving the preview window if ( e.which === 9 ) { // 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 8 : $keyboard.keyaction.bksp(base, null, e); e.preventDefault(); break; case 13: $keyboard.keyaction.enter(base, null, e); break; // Show capsLock case 20: base.shiftActive = base.capsLock = !base.capsLock; base.showSet(); break; case 86: // prevent ctrl-v/cmd-v if (e.ctrlKey || e.metaKey) { if (o.preventPaste) { e.preventDefault(); return; } base.checkCombos(); // check pasted content } break; } }) .bind('mouseup touchend '.split(' ').join(base.namespace + ' '), function(){ base.last.virtual = true; if (base.checkCaret) { base.saveCaret(); } }); // prevent keyboard event bubbling base.$keyboard.bind('mousedown click touchstart '.split(' ').join(base.namespace + ' '), function(e){ e.stopPropagation(); if (!base.isCurrent()) { base.reveal(); $(document).trigger('checkkeyboard' + base.namespace); } if (!o.noFocus) { base.$preview.focus(); } }); // If preventing paste, block context menu (right click) if (o.preventPaste){ base.$preview.bind('contextmenu' + base.namespace, function(e){ e.preventDefault(); }); base.$el.bind('contextmenu' + base.namespace, function(e){ e.preventDefault(); }); } }; base.bindKeys = function(){ var kbcss = $keyboard.css; base.$allKeys = base.$keyboard.find('button.' + kbcss.keyButton) .unbind(base.namespace + ' ' + base.namespace + 'kb') .bind(o.keyBinding.split(' ').join(base.namespace + ' ') + base.namespace + ' ' + $keyboard.events.kbRepeater, function(e){ e.preventDefault(); // prevent errors when external triggers attempt to 'type' - see issue #158 if (!base.$keyboard.is(':visible')){ return false; } var action, $keys, last = base.last, key = this, $key = $(key), // prevent mousedown & touchstart from both firing events at the same time - see #184 timer = new Date().getTime(); if ( o.useWheel && base.wheel ) { // get keys from other layers/keysets (shift, alt, meta, etc) that line up by data-position $keys = last.wheel_$Keys; // target mousewheel selected key $key = $keys ? $keys.eq( last.wheelIndex ) : $key; } action = $key.attr( 'data-action' ); // don't split colon key. Fixes #264 action = action === ':' ? ':' : (action || '').split(':')[0]; if ( timer - ( last.eventTime || 0 ) < o.preventDoubleEventTime ) { return; } last.eventTime = timer; last.event = e; last.virtual = true; if (!o.noFocus) { base.$preview.focus(); } last.$key = $key; last.key = $key.attr('data-value'); // Start caret in IE when not focused (happens with each virtual keyboard button click if (base.checkCaret) { $keyboard.caret( base.$preview, last ); } if (action.match('meta')) { action = 'meta'; } // keyaction is added as a string, override original action & text if (action === last.key && typeof $keyboard.keyaction[ action ] === 'string' ) { last.key = action = $keyboard.keyaction[ action ]; } else if (action in $keyboard.keyaction && $.isFunction($keyboard.keyaction[action])) { // stop processing if action returns false (close & cancel) if ( $keyboard.keyaction[ action ]( base, this, e ) === false ) { return false; } action = null; // prevent inserting action name } if (typeof action !== 'undefined' && action !== null) { last.key = $(this).hasClass(kbcss.keyAction) ? action : last.key; base.insertText( last.key ); if (!base.capsLock && !o.stickyShift && !e.shiftKey) { base.shiftActive = false; base.showSet( $key.attr('data-name') ); } } // set caret if caret moved by action function; also, attempt to fix issue #131 $keyboard.caret( base.$preview, last ); base.checkCombos(); base.$el.trigger( $keyboard.events.kbChange, [ base, base.el ] ); last.val = base.$preview.val(); if ($.isFunction(o.change)){ o.change( $.Event( $keyboard.events.inputChange ), base, base.el ); // return false to prevent reopening keyboard if base.accept() was called return false; } }) // Change hover class and tooltip .bind('mouseenter mouseleave touchstart '.split(' ').join(base.namespace + ' '), function( e ) { if (!base.isCurrent()) { return; } var $keys, txt, last = base.last, $this = $(this), type = e.type; if ( o.useWheel && base.wheel ) { $keys = base.getLayers( $this ); txt = ( $keys.length ? $keys.map( function() { return $( this ).attr( 'data-value' ) || ''; }).get() : '' ) || [ $this.text() ]; last.wheel_$Keys = $keys; last.wheelLayers = txt; last.wheelIndex = $.inArray( $this.attr( 'data-value' ), txt ); } if ((type === 'mouseenter' || type === 'touchstart') && base.el.type !== 'password' && !$this.hasClass(o.css.buttonDisabled) ) { $this.addClass(o.css.buttonHover); if ( o.useWheel && base.wheel ) { $this.attr( 'title', function( i, t ) { // show mouse wheel message return ( base.wheel && t === '' && base.sets && txt.length > 1 && type !== 'touchstart' ) ? o.wheelMessage : t; }); } } if ( type === 'mouseleave' ) { // needed or IE flickers really bad $this.removeClass( (base.el.type === 'password') ? '' : o.css.buttonHover); if ( o.useWheel && base.wheel ) { last.wheelIndex = 0; last.wheelLayers = []; last.wheel_$Keys = null; $this .attr( 'title', function( i, t ){ return ( t === o.wheelMessage ) ? '' : t; }) .html( $this.attr('data-html') ); // restore original button text } } }) // 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.isVisible() && base.isCurrent()) { base.$preview.focus(); } if (base.checkCaret) { $keyboard.caret( base.$preview, base.last ); } } base.mouseRepeat = [false,'']; clearTimeout(base.repeater); // make sure key repeat stops! return false; }) // prevent form submits when keyboard is bound locally - issue #64 .bind('click' + base.namespace, function(){ return false; }) // no mouse repeat for action keys (shift, ctrl, alt, meta, etc) .not( '.' + kbcss.keyAction ) // Allow mousewheel to scroll through other keysets of the same (non-action) key .bind( 'mousewheel' + base.namespace, function( e, delta ) { if ( o.useWheel && base.wheel ) { // deltaY used by newer versions of mousewheel plugin delta = delta || e.deltaY; var n, txt = base.last.wheelLayers || []; if ( txt.length > 1 ) { n = base.last.wheelIndex + ( delta > 0 ? -1 : 1 ); if ( n > txt.length-1) { n = 0; } if ( n < 0 ) { n = txt.length-1; } } else { n = 0; } base.last.wheelIndex = n; $( this ).html( txt[ n ] ); return false; } }) // mouse repeated action key exceptions .add('.' + kbcss.keyPrefix + ('tab bksp space enter'.split(' ').join(',.' + kbcss.keyPrefix)), base.$keyboard) .bind('mousedown touchstart '.split(' ').join(base.namespace + 'kb '), function(){ if (o.repeatRate !== 0) { var key = $(this); base.mouseRepeat = [true, key]; // save the key, make sure we are repeating the right one (fast typers) setTimeout(function() { if (base.mouseRepeat[0] && base.mouseRepeat[1] === key) { base.repeatKey(key); } }, o.repeatDelay); } return false; }); }; // Insert text at caret/selection - thanks to Derek Wickwire for fixing this up! base.insertText = function(txt){ if ( typeof txt === 'undefined' ) { return; } var bksp, t, isBksp = txt === '\b', // use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE). val = base.$preview.val(), pos = $keyboard.caret( base.$preview ), len = val.length; // save original content length // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea // is still difficult // in IE, pos.end can be zero after input loses focus if (pos.end < pos.start) { pos.end = pos.start; } if (pos.start > len) { pos.end = pos.start = len; } if (base.preview.nodeName === 'TEXTAREA') { // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine) if ($keyboard.msie && val.substr(pos.start, 1) === '\n') { pos.start += 1; pos.end += 1; } } if (txt === '{d}') { txt = ''; t = pos.start; pos.end += 1; } bksp = isBksp && pos.start === pos.end; txt = isBksp ? '' : txt; val = val.substr(0, pos.start - (bksp ? 1 : 0)) + txt + val.substr(pos.end); t = pos.start + (bksp ? -1 : txt.length); base.$preview.val( val ); base.saveCaret( t, t ); // save caret in case of bksp base.setScroll(); }; // check max length base.checkMaxLength = function(){ var start, caret, val = base.$preview.val(); if (o.maxLength !== false && val.length > o.maxLength) { start = $keyboard.caret( base.$preview ).start; caret = Math.min(start, o.maxLength); // prevent inserting new characters when maxed #289 if (!o.maxInsert) { val = base.last.val; caret = start - 1; // move caret back one } base.$preview.val( val.substring(0, o.maxLength) ); // restore caret on change, otherwise it ends up at the end. base.saveCaret( caret, caret ); } if (base.$decBtn.length) { base.checkDecimal(); } }; // mousedown repeater base.repeatKey = function(key){ key.trigger( $keyboard.events.kbRepeater ); if (base.mouseRepeat[0]) { base.repeater = setTimeout(function() { base.repeatKey(key); }, base.repeatTime); } }; // make it easier to switch keysets via API // showKeySet('shift+alt+meta1') base.showKeySet = function(str) { if (typeof str === 'string') { base.last.keyset = [ base.shiftActive, base.altActive, base.metaActive ]; base.shiftActive = /shift/i.test(str); base.altActive = /alt/i.test(str); if (/meta/.test(str)) { base.metaActive = true; base.showSet( str.match(/meta\d+/i)[0] ); } else { base.metaActive = false; base.showSet(); } } else { base.showSet( str ); } }; base.showSet = function( name ) { o = base.options; // refresh options var kbcss = $keyboard.css, prefix = '.' + kbcss.keyPrefix, active = o.css.buttonActive, key = '', toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0); if (!base.shiftActive) { base.capsLock = false; } // check meta key set if (base.metaActive) { // the name attribute contains the meta set # 'meta99' key = (/meta/i.test(name)) ? name : ''; // save active meta keyset name if (key === '') { key = (base.metaActive === true) ? '' : base.metaActive; } else { base.metaActive = key; } // if meta keyset doesn't have a shift or alt keyset, then show just the meta key set if ( (!o.stickyShift && base.last.keyset[2] !== base.metaActive) || ( (base.shiftActive || base.altActive) && !base.$keyboard.find('.' + kbcss.keySet + '-' + key + base.rows[toShow]).length) ) { base.shiftActive = base.altActive = false; } } else if (!o.stickyShift && base.last.keyset[2] !== base.metaActive && base.shiftActive) { // switching from meta key set back to default, reset shift & alt if using stickyShift base.shiftActive = base.altActive = false; } toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0); key = (toShow === 0 && !base.metaActive) ? '-normal' : (key === '') ? '' : '-' + key; if (!base.$keyboard.find('.' + kbcss.keySet + key + base.rows[toShow]).length) { // keyset doesn't exist, so restore last keyset settings base.shiftActive = base.last.keyset[0]; base.altActive = base.last.keyset[1]; base.metaActive = base.last.keyset[2]; return; } base.$keyboard .find( prefix + 'alt,' + prefix + 'shift,.' + kbcss.keyAction + '[class*=meta]' ) .removeClass( active ).end() .find( prefix + 'alt' ).toggleClass( active, base.altActive ).end() .find( prefix + 'shift' ).toggleClass( active, base.shiftActive ).end() .find( prefix + 'lock' ).toggleClass( active, base.capsLock ).end() .find( '.' + kbcss.keySet ).hide().end() .find( '.' + kbcss.keySet + key + base.rows[toShow] ).show().end() .find( '.' + kbcss.keyAction + prefix + key ).addClass( active ); if ( base.metaActive ) { base.$keyboard.find( prefix + base.metaActive ) // base.metaActive contains the string "meta#" or false // without the !== false, jQuery UI tries to transition the classes .toggleClass( active, base.metaActive !== false ); } base.last.keyset = [ base.shiftActive, base.altActive, base.metaActive ]; base.$el.trigger( $keyboard.events.kbKeysetChange, [ base, base.el ] ); }; // check for key combos (dead keys) base.checkCombos = function(){ if (!base.isVisible()) { return base.$preview.val(); } var i, r, t, t2, // use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE). val = base.$preview.val(), pos = $keyboard.caret( base.$preview ), layout = $keyboard.builtLayouts[base.layout], len = val.length; // save original content length // return if val is empty; fixes #352 if (val === '') { return val; } // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea // is still difficult // in IE, pos.end can be zero after input loses focus if (pos.end < pos.start) { pos.end = pos.start; } if (pos.start > len) { pos.end = pos.start = len; } // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine) if ($keyboard.msie && val.substr(pos.start, 1) === '\n') { pos.start += 1; pos.end += 1; } if (o.useCombos) { // keep 'a' and 'o' in the regex for ae and oe ligature (æ,œ) // thanks to KennyTM: http://stackoverflow.com/q/4275077 // original regex /([`\'~\^\"ao])([a-z])/mig moved to $.keyboard.comboRegex if ($keyboard.msie) { // old IE may not have the caret positioned correctly, so just check the whole thing val = val.replace(base.regex, function(s, accent, letter){ return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s; }); // prevent combo replace error, in case the keyboard closes - see issue #116 } else if (base.$preview.length) { // Modern browsers - check for combos from last two characters left of the caret t = pos.start - (pos.start - 2 >= 0 ? 2 : 0); // target last two characters $keyboard.caret( base.$preview, t, pos.end ); // do combo replace t2 = ($keyboard.caret( base.$preview ).text || '').replace(base.regex, function(s, accent, letter){ return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s; }); // add combo back base.$preview.val( $keyboard.caret( base.$preview ).replaceStr(t2) ); val = base.$preview.val(); } } // check input restrictions - in case content was pasted if (o.restrictInput && val !== '') { t = val; r = layout.acceptedKeys.length; for (i=0; i < r; i++){ if (t === '') { continue; } t2 = layout.acceptedKeys[i]; if (val.indexOf(t2) >= 0) { // escape out all special characters if (/[\[|\]|\\|\^|\$|\.|\||\?|\*|\+|\(|\)|\{|\}]/g.test(t2)) { t2 = '\\' + t2; } t = t.replace( (new RegExp(t2, 'g')), ''); } } // what's left over are keys that aren't in the acceptedKeys array if (t !== '') { val = val.replace(t, ''); } } // save changes, then reposition caret pos.start += val.length - len; pos.end += val.length - len; base.$preview.val(val); base.saveCaret( pos.start, pos.end ); // set scroll to keep caret in view base.setScroll(); base.checkMaxLength(); if (o.acceptValid) { base.checkValid(); } return val; // return text, used for keyboard closing section }; // Toggle accept button classes, if validating base.checkValid = function(){ var kbcss = $keyboard.css, valid = true; if ($.isFunction(o.validate)) { valid = o.validate(base, base.$preview.val(), false); } // toggle accept button classes; defined in the css base.$keyboard.find('.' + kbcss.keyPrefix + 'accept') .toggleClass( kbcss.inputInvalid, !valid ) .toggleClass( kbcss.inputValid, valid ); }; // Decimal button for num pad - only allow one (not used by default) base.checkDecimal = function(){ // Check US '.' or European ',' format if ( ( base.decimal && /\./g.test(base.preview.value) ) || ( !base.decimal && /\,/g.test(base.preview.value) ) ) { base.$decBtn .attr({ 'disabled': 'disabled', 'aria-disabled': 'true' }) .removeClass(o.css.buttonHover) .addClass(o.css.buttonDisabled); } else { base.$decBtn .removeAttr('disabled') .attr({ 'aria-disabled': 'false' }) .addClass(o.css.buttonDefault) .removeClass(o.css.buttonDisabled); } }; // get other layer values for a specific key base.getLayers = function($el){ var kbcss = $keyboard.css, key = $el.attr('data-pos'), $keys = $el.closest('.' + kbcss.keyboard).find('button[data-pos="' + key + '"]'); return $keys.filter(function(){ return $(this).find('.' + kbcss.keyText).text() !== ''; }).add($el); }; // Go to next or prev inputs // goToNext = true, then go to next input; if false go to prev // isAccepted is from autoAccept option or true if user presses shift+enter base.switchInput = function(goToNext, isAccepted){ if ($.isFunction(o.switchInput)) { o.switchInput(base, goToNext, isAccepted); } else { // base.$keyboard may be an empty array - see #275 (apod42) if (base.$keyboard.length) { base.$keyboard.hide(); } var kb, stopped = false, all = $('button, input, textarea, a').filter(':visible').not(':disabled'), indx = all.index(base.$el) + (goToNext ? 1 : -1); if (base.$keyboard.length) { base.$keyboard.show(); } if (indx > all.length - 1) { stopped = o.stopAtEnd; indx = 0; // go to first input } if (indx < 0) { stopped = o.stopAtEnd; indx = all.length - 1; // stop or go to last } if (!stopped) { isAccepted = base.close(isAccepted); if (!isAccepted) { return; } kb = all.eq(indx).data('keyboard'); if (kb && kb.options.openOn.length) { kb.focusOn(); } else { all.eq(indx).focus(); } } } return false; }; // Close the keyboard, if visible. Pass a status of true, if the content was accepted // (for the event trigger). base.close = function(accepted){ if (base.isOpen) { 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 = false; // update value for always open keyboards base.$preview.val(val); base.$el .removeClass(kbcss.isCurrent + ' ' + kbcss.inputAutoAccepted) // add 'ui-keyboard-autoaccepted' to inputs - see issue #66 .addClass( (accepted || false) ? accepted === true ? '' : kbcss.inputAutoAccepted : '' ) .val( val ) // trigger default change event - see issue #146 .trigger(kbevents.inputChange) // don't trigger beforeClose if keyboard is always open .trigger( (o.alwaysOpen) ? '' : kbevents.kbBeforeClose, [ base, base.el, (accepted || false) ] ) .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.openOn) { // rebind input focus - delayed to fix IE issue #72 base.timer = setTimeout(function(){ // 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(); } } }, 500); } if (!o.alwaysOpen && base.$keyboard) { // free up memory base.$keyboard.remove(); base.$keyboard = []; } if (!base.watermark && base.el.value === '' && base.inPlaceholder !== '') { base.$el .addClass(kbcss.placeholder) .val(base.inPlaceholder); } } } return !!accepted; }; base.accept = function(){ return base.close(true); }; base.escClose = function(e){ if ( e && e.type === 'keyup' ) { return ( e.which === 27 && !o.ignoreEsc ) ? base.close( o.autoAccept && o.autoAcceptOnEsc ? 'true' : false ) : ''; } // keep keyboard open if alwaysOpen or stayOpen is true - fixes mutliple always open keyboards or // single stay open keyboard if ( !base.isOpen ) { return; } // ignore autoaccept if using escape - good idea? if ( !base.isCurrent() && base.isOpen || base.isOpen && e.target !== base.el ) { // don't close if stayOpen is set; but close if a different keyboard is being opened if (o.stayOpen && !$(e.target).hasClass('ui-keyboard-input')) { return; } // stop propogation in IE - an input getting focus doesn't open a keyboard if one is already open if ( $keyboard.allie ) { e.preventDefault(); } // send 'true' instead of a true (boolean), the input won't get a 'ui-keyboard-autoaccepted' // class name - see issue #66 base.close( o.autoAccept ? 'true' : false ); } }; // Build default button base.keyBtn = $('