3494 lines
108 KiB
JavaScript
3494 lines
108 KiB
JavaScript
/*! 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
|
|
$('<div />')
|
|
.addClass(kbcss.wrapper)
|
|
.append(base.$preview)
|
|
.prependTo(base.$keyboard);
|
|
} else {
|
|
base.$preview = base.$el;
|
|
base.preview = base.el;
|
|
}
|
|
};
|
|
|
|
// Added in v1.26.8 to allow chaining of the caret function, e.g.
|
|
// keyboard.reveal().caret(4,5).insertText('test').caret('end');
|
|
base.caret = function(param1, param2) {
|
|
var result = $keyboard.caret(base.$preview, param1, param2),
|
|
wasSetCaret = result instanceof $;
|
|
// Caret was set, save last position & make chainable
|
|
if (wasSetCaret) {
|
|
base.saveCaret(result.start, result.end);
|
|
return base;
|
|
}
|
|
// return caret position if using .caret()
|
|
return result;
|
|
};
|
|
|
|
base.saveCaret = function (start, end, $el) {
|
|
if (base.isCurrent()) {
|
|
var p;
|
|
if (typeof start === 'undefined') {
|
|
// grab & save current caret position
|
|
p = $keyboard.caret($el || base.$preview);
|
|
} else {
|
|
p = $keyboard.caret($el || base.$preview, start, end);
|
|
}
|
|
base.last.start = typeof start === 'undefined' ? p.start : start;
|
|
base.last.end = typeof end === 'undefined' ? p.end : end;
|
|
}
|
|
};
|
|
|
|
base.saveLastChange = function (val) {
|
|
base.last.val = val || base.getValue(base.$preview || base.$el);
|
|
if (base.isContentEditable) {
|
|
base.last.elms = base.el.cloneNode(true);
|
|
}
|
|
};
|
|
|
|
base.setScroll = function () {
|
|
// Set scroll so caret & current text is in view
|
|
// needed for virtual keyboard typing, NOT manual typing - fixes #23
|
|
if (!base.isContentEditable && base.last.virtual) {
|
|
|
|
var scrollWidth, clientWidth, adjustment, direction,
|
|
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 = $('<button />')
|
|
.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 = '<span class="' + kbcss.keyText + '">' +
|
|
// this prevents HTML from being added to the key
|
|
keys.name.replace(/[\u00A0-\u9999]/gim, function (i) {
|
|
return '&#' + i.charCodeAt(0) + ';';
|
|
}) +
|
|
'</span>';
|
|
|
|
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 = $('<div />')
|
|
.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 = $('<div />')
|
|
.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('<br class="' + kbcss.endRow + '"/>');
|
|
}
|
|
}
|
|
});
|
|
|
|
if (sets > 1) {
|
|
base.sets = true;
|
|
}
|
|
layout.hasMappedKeys = !($.isEmptyObject(layout.mappedKeys));
|
|
layout.$keyboard = container;
|
|
return container;
|
|
};
|
|
|
|
base.buildRow = function ($row, row, keys, acceptedKeys) {
|
|
var t, txt, key, isAction, action, margin,
|
|
kbcss = $keyboard.css;
|
|
for (key = 0; key < keys.length; key++) {
|
|
// used by addKey function
|
|
base.temp = [$row, row, key];
|
|
isAction = false;
|
|
|
|
// ignore empty keys
|
|
if (keys[key].length === 0) {
|
|
continue;
|
|
}
|
|
|
|
// process here if it's an action key
|
|
if (/^\{\S+\}$/.test(keys[key])) {
|
|
action = keys[key].match(/^\{(\S+)\}$/)[1];
|
|
// add active class if there are double exclamation points in the name
|
|
if (/\!\!/.test(action)) {
|
|
action = action.replace('!!', '');
|
|
isAction = true;
|
|
}
|
|
|
|
// add empty space
|
|
if (/^sp:((\d+)?([\.|,]\d+)?)(em|px)?$/i.test(action)) {
|
|
// not perfect globalization, but allows you to use {sp:1,1em}, {sp:1.2em} or {sp:15px}
|
|
margin = parseFloat(action
|
|
.replace(/,/, '.')
|
|
.match(/^sp:((\d+)?([\.|,]\d+)?)(em|px)?$/i)[1] || 0
|
|
);
|
|
$('<span class="' + kbcss.keyText + '"></span>')
|
|
// previously {sp:1} would add 1em margin to each side of a 0 width span
|
|
// now Firefox doesn't seem to render 0px dimensions, so now we set the
|
|
// 1em margin x 2 for the width
|
|
.width((action.match(/px/i) ? margin + 'px' : (margin * 2) + 'em'))
|
|
.addClass(kbcss.keySpacer)
|
|
.appendTo($row);
|
|
}
|
|
|
|
// add empty button
|
|
if (/^empty(:((\d+)?([\.|,]\d+)?)(em|px)?)?$/i.test(action)) {
|
|
margin = (/:/.test(action)) ? parseFloat(action
|
|
.replace(/,/, '.')
|
|
.match(/^empty:((\d+)?([\.|,]\d+)?)(em|px)?$/i)[1] || 0
|
|
) : '';
|
|
base
|
|
.addKey('', ' ', true)
|
|
.addClass(o.css.buttonDisabled + ' ' + o.css.buttonEmpty)
|
|
.attr('aria-disabled', true)
|
|
.width(margin ? (action.match('px') ? margin + 'px' : (margin * 2) + 'em') : '');
|
|
continue;
|
|
}
|
|
|
|
// meta keys
|
|
if (/^meta[\w-]+\:?(\w+)?/i.test(action)) {
|
|
base
|
|
.addKey(action.split(':')[0], action)
|
|
.addClass(kbcss.keyHasActive);
|
|
continue;
|
|
}
|
|
|
|
// switch needed for action keys with multiple names/shortcuts or
|
|
// default will catch all others
|
|
txt = action.split(':');
|
|
switch (txt[0].toLowerCase()) {
|
|
|
|
case 'a':
|
|
case 'accept':
|
|
base
|
|
.addKey('accept', action)
|
|
.addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
|
|
break;
|
|
|
|
case 'alt':
|
|
case 'altgr':
|
|
base
|
|
.addKey('alt', action)
|
|
.addClass(kbcss.keyHasActive);
|
|
break;
|
|
|
|
case 'b':
|
|
case 'bksp':
|
|
base.addKey('bksp', action);
|
|
break;
|
|
|
|
case 'c':
|
|
case 'cancel':
|
|
base
|
|
.addKey('cancel', action)
|
|
.addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
|
|
break;
|
|
|
|
// toggle combo/diacritic key
|
|
/*jshint -W083 */
|
|
case 'combo':
|
|
base
|
|
.addKey('combo', action)
|
|
.addClass(kbcss.keyHasActive)
|
|
.attr('title', function (indx, title) {
|
|
// add combo key state to title
|
|
return title + ' ' + o.display[o.useCombos ? 'active' : 'disabled'];
|
|
})
|
|
.toggleClass(o.css.buttonActive, o.useCombos);
|
|
break;
|
|
|
|
// Decimal - unique decimal point (num pad layout)
|
|
case 'dec':
|
|
acceptedKeys.push((base.decimal) ? '.' : ',');
|
|
base.addKey('dec', action);
|
|
break;
|
|
|
|
case 'e':
|
|
case 'enter':
|
|
base
|
|
.addKey('enter', action)
|
|
.addClass(o.css.buttonAction + ' ' + kbcss.keyAction);
|
|
break;
|
|
|
|
case 'lock':
|
|
base
|
|
.addKey('lock', action)
|
|
.addClass(kbcss.keyHasActive);
|
|
break;
|
|
|
|
case 's':
|
|
case 'shift':
|
|
base
|
|
.addKey('shift', action)
|
|
.addClass(kbcss.keyHasActive);
|
|
break;
|
|
|
|
// Change sign (for num pad layout)
|
|
case 'sign':
|
|
acceptedKeys.push('-');
|
|
base.addKey('sign', action);
|
|
break;
|
|
|
|
case 'space':
|
|
acceptedKeys.push(' ');
|
|
base.addKey('space', action);
|
|
break;
|
|
|
|
case 't':
|
|
case 'tab':
|
|
base.addKey('tab', action);
|
|
break;
|
|
|
|
default:
|
|
if ($keyboard.keyaction.hasOwnProperty(txt[0])) {
|
|
base
|
|
.addKey(txt[0], action)
|
|
.toggleClass(o.css.buttonAction + ' ' + kbcss.keyAction, isAction);
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// regular button (not an action key)
|
|
t = keys[key];
|
|
base.addKey(t, t, true);
|
|
}
|
|
}
|
|
};
|
|
|
|
base.removeBindings = function (namespace) {
|
|
$(document).unbind(namespace);
|
|
if (base.el.ownerDocument !== document) {
|
|
$(base.el.ownerDocument).unbind(namespace);
|
|
}
|
|
$(window).unbind(namespace);
|
|
base.$el.unbind(namespace);
|
|
};
|
|
|
|
base.removeKeyboard = function () {
|
|
base.$allKeys = [];
|
|
base.$decBtn = [];
|
|
// base.$preview === base.$el when o.usePreview is false - fixes #442
|
|
if (o.usePreview) {
|
|
base.$preview.removeData('keyboard');
|
|
}
|
|
base.$preview.unbind(base.namespace + 'keybindings');
|
|
base.preview = null;
|
|
base.$preview = null;
|
|
base.$previewCopy = null;
|
|
base.$keyboard.removeData('keyboard');
|
|
base.$keyboard.remove();
|
|
base.$keyboard = [];
|
|
base.isOpen = false;
|
|
base.isCurrent(false);
|
|
};
|
|
|
|
base.destroy = function (callback) {
|
|
var index,
|
|
kbcss = $keyboard.css,
|
|
len = base.extensionNamespace.length,
|
|
tmp = [
|
|
kbcss.input,
|
|
kbcss.locked,
|
|
kbcss.placeholder,
|
|
kbcss.noKeyboard,
|
|
kbcss.alwaysOpen,
|
|
o.css.input,
|
|
kbcss.isCurrent
|
|
].join(' ');
|
|
clearTimeout(base.timer);
|
|
clearTimeout(base.timer2);
|
|
clearTimeout(base.timer3);
|
|
if (base.$keyboard.length) {
|
|
base.removeKeyboard();
|
|
}
|
|
base.removeBindings(base.namespace);
|
|
base.removeBindings(base.namespace + 'callbacks');
|
|
for (index = 0; index < len; index++) {
|
|
base.removeBindings(base.extensionNamespace[index]);
|
|
}
|
|
base.el.active = false;
|
|
|
|
base.$el
|
|
.removeClass(tmp)
|
|
.removeAttr('aria-haspopup')
|
|
.removeAttr('role')
|
|
.removeData('keyboard');
|
|
base = null;
|
|
|
|
if (typeof callback === 'function') {
|
|
callback();
|
|
}
|
|
};
|
|
|
|
// Run initializer
|
|
base.init();
|
|
|
|
}; // end $.keyboard definition
|
|
|
|
// event.which & ASCII values
|
|
$keyboard.keyCodes = {
|
|
backSpace: 8,
|
|
tab: 9,
|
|
enter: 13,
|
|
capsLock: 20,
|
|
escape: 27,
|
|
space: 32,
|
|
pageUp: 33,
|
|
pageDown: 34,
|
|
end: 35,
|
|
home: 36,
|
|
left: 37,
|
|
up: 38,
|
|
right: 39,
|
|
down: 40,
|
|
insert: 45,
|
|
delete: 46,
|
|
// event.which keyCodes (uppercase letters)
|
|
A: 65,
|
|
Z: 90,
|
|
V: 86,
|
|
C: 67,
|
|
X: 88,
|
|
|
|
// ASCII lowercase a & z
|
|
a: 97,
|
|
z: 122
|
|
};
|
|
|
|
$keyboard.css = {
|
|
// keyboard id suffix
|
|
idSuffix: '_keyboard',
|
|
// class name to set initial focus
|
|
initialFocus: 'keyboard-init-focus',
|
|
// element class names
|
|
input: 'ui-keyboard-input',
|
|
inputClone: 'ui-keyboard-preview-clone',
|
|
wrapper: 'ui-keyboard-preview-wrapper',
|
|
preview: 'ui-keyboard-preview',
|
|
keyboard: 'ui-keyboard',
|
|
keySet: 'ui-keyboard-keyset',
|
|
keyButton: 'ui-keyboard-button',
|
|
keyWide: 'ui-keyboard-widekey',
|
|
keyPrefix: 'ui-keyboard-',
|
|
keyText: 'ui-keyboard-text', // span with button text
|
|
keyHasActive: 'ui-keyboard-hasactivestate',
|
|
keyAction: 'ui-keyboard-actionkey',
|
|
keySpacer: 'ui-keyboard-spacer', // empty keys
|
|
keyToggle: 'ui-keyboard-toggle',
|
|
keyDisabled: 'ui-keyboard-disabled',
|
|
// Class for BRs with a div wrapper inside of contenteditable
|
|
divWrapperCE: 'ui-keyboard-div-wrapper',
|
|
// states
|
|
locked: 'ui-keyboard-lockedinput',
|
|
alwaysOpen: 'ui-keyboard-always-open',
|
|
noKeyboard: 'ui-keyboard-nokeyboard',
|
|
placeholder: 'ui-keyboard-placeholder',
|
|
hasFocus: 'ui-keyboard-has-focus',
|
|
isCurrent: 'ui-keyboard-input-current',
|
|
// validation & autoaccept
|
|
inputValid: 'ui-keyboard-valid-input',
|
|
inputInvalid: 'ui-keyboard-invalid-input',
|
|
inputAutoAccepted: 'ui-keyboard-autoaccepted',
|
|
endRow: 'ui-keyboard-button-endrow' // class added to <br>
|
|
};
|
|
|
|
$keyboard.events = {
|
|
// keyboard events
|
|
kbChange: 'keyboardChange',
|
|
kbBeforeClose: 'beforeClose',
|
|
kbBeforeVisible: 'beforeVisible',
|
|
kbVisible: 'visible',
|
|
kbInit: 'initialized',
|
|
kbInactive: 'inactive',
|
|
kbHidden: 'hidden',
|
|
kbRepeater: 'repeater',
|
|
kbKeysetChange: 'keysetChange',
|
|
// input events
|
|
inputAccepted: 'accepted',
|
|
inputCanceled: 'canceled',
|
|
inputChange: 'change',
|
|
inputRestricted: 'restricted'
|
|
};
|
|
|
|
// Action key function list
|
|
$keyboard.keyaction = {
|
|
accept: function (base) {
|
|
base.close(true); // same as base.accept();
|
|
return false; // return false prevents further processing
|
|
},
|
|
alt: function (base) {
|
|
base.altActive = !base.altActive;
|
|
base.showSet();
|
|
},
|
|
bksp: function (base) {
|
|
if (base.isContentEditable) {
|
|
base.execCommand('delete');
|
|
// save new caret position
|
|
base.saveCaret();
|
|
} else {
|
|
// the script looks for the '\b' string and initiates a backspace
|
|
base.insertText('\b');
|
|
}
|
|
},
|
|
cancel: function (base) {
|
|
base.close();
|
|
return false; // return false prevents further processing
|
|
},
|
|
clear: function (base) {
|
|
base.$preview[base.isContentEditable ? 'text' : 'val']('');
|
|
if (base.$decBtn.length) {
|
|
base.checkDecimal();
|
|
}
|
|
},
|
|
combo: function (base) {
|
|
var o = base.options,
|
|
c = !o.useCombos,
|
|
$combo = base.$keyboard.find('.' + $keyboard.css.keyPrefix + 'combo');
|
|
o.useCombos = c;
|
|
$combo
|
|
.toggleClass(o.css.buttonActive, c)
|
|
// update combo key state
|
|
.attr('title', $combo.attr('data-title') + ' (' + o.display[c ? 'active' : 'disabled'] + ')');
|
|
if (c) {
|
|
base.checkCombos();
|
|
}
|
|
return false;
|
|
},
|
|
dec: function (base) {
|
|
base.insertText((base.decimal) ? '.' : ',');
|
|
},
|
|
del: function (base) {
|
|
if (base.isContentEditable) {
|
|
base.execCommand('forwardDelete');
|
|
} else {
|
|
// the script looks for the '{d}' string and initiates a delete
|
|
base.insertText('{d}');
|
|
}
|
|
},
|
|
// resets to base keyset (deprecated because "default" is a reserved word)
|
|
'default': function (base) {
|
|
base.shiftActive = base.altActive = base.metaActive = false;
|
|
base.showSet();
|
|
},
|
|
// el is the pressed key (button) object; it is null when the real keyboard enter is pressed
|
|
enter: function (base, el, e) {
|
|
var tag = base.el.nodeName,
|
|
o = base.options;
|
|
// shift+enter in textareas
|
|
if (e.shiftKey) {
|
|
// textarea, input & contenteditable - enterMod + shift + enter = accept,
|
|
// then go to prev; base.switchInput(goToNext, autoAccept)
|
|
// textarea & input - shift + enter = accept (no navigation)
|
|
return (o.enterNavigation) ? base.switchInput(!e[o.enterMod], true) : base.close(true);
|
|
}
|
|
// input only - enterMod + enter to navigate
|
|
if (o.enterNavigation && (tag !== 'TEXTAREA' || e[o.enterMod])) {
|
|
return base.switchInput(!e[o.enterMod], o.autoAccept ? 'true' : false);
|
|
}
|
|
// pressing virtual enter button inside of a textarea - add a carriage return
|
|
// e.target is span when clicking on text and button at other times
|
|
if (tag === 'TEXTAREA' && $(e.target).closest('button').length) {
|
|
// IE8 fix (space + \n) - fixes #71 thanks Blookie!
|
|
base.insertText(($keyboard.msie ? ' ' : '') + '\n');
|
|
}
|
|
if (base.isContentEditable && !o.enterNavigation) {
|
|
base.execCommand('insertHTML', '<div><br class="' + $keyboard.css.divWrapperCE + '"></div>');
|
|
// Using backspace on wrapped BRs will now shift the textnode inside of the wrapped BR
|
|
// Although not ideal, the caret is moved after the block - see the wiki page for
|
|
// more details: https://github.com/Mottie/Keyboard/wiki/Contenteditable#limitations
|
|
// move caret after a delay to allow rendering of HTML
|
|
setTimeout(function() {
|
|
$keyboard.keyaction.right(base);
|
|
base.saveCaret();
|
|
}, 0);
|
|
}
|
|
},
|
|
// caps lock key
|
|
lock: function (base) {
|
|
base.last.keyset[0] = base.shiftActive = base.capsLock = !base.capsLock;
|
|
base.showSet();
|
|
},
|
|
left: function (base) {
|
|
var p = $keyboard.caret(base.$preview);
|
|
if (p.start - 1 >= 0) {
|
|
// move both start and end of caret (prevents text selection) & save caret position
|
|
base.last.start = base.last.end = p.start - 1;
|
|
$keyboard.caret(base.$preview, base.last);
|
|
base.setScroll();
|
|
}
|
|
},
|
|
meta: function (base, el) {
|
|
var $el = $(el);
|
|
base.metaActive = !$el.hasClass(base.options.css.buttonActive);
|
|
base.showSet($el.attr('data-name'));
|
|
},
|
|
next: function (base) {
|
|
base.switchInput(true, base.options.autoAccept);
|
|
return false;
|
|
},
|
|
// same as 'default' - resets to base keyset
|
|
normal: function (base) {
|
|
base.shiftActive = base.altActive = base.metaActive = false;
|
|
base.showSet();
|
|
},
|
|
prev: function (base) {
|
|
base.switchInput(false, base.options.autoAccept);
|
|
return false;
|
|
},
|
|
right: function (base) {
|
|
var p = $keyboard.caret(base.$preview),
|
|
len = base.isContentEditable ? $keyboard.getEditableLength(base.el) : base.getValue().length;
|
|
if (p.end + 1 <= len) {
|
|
// move both start and end of caret to end position
|
|
// (prevents text selection) && save caret position
|
|
base.last.start = base.last.end = p.end + 1;
|
|
$keyboard.caret(base.$preview, base.last);
|
|
base.setScroll();
|
|
}
|
|
},
|
|
shift: function (base) {
|
|
base.last.keyset[0] = base.shiftActive = !base.shiftActive;
|
|
base.showSet();
|
|
},
|
|
sign: function (base) {
|
|
if (/^[+-]?\d*\.?\d*$/.test(base.getValue())) {
|
|
var caret,
|
|
p = $keyboard.caret(base.$preview),
|
|
val = base.getValue(),
|
|
len = base.isContentEditable ? $keyboard.getEditableLength(base.el) : val.length;
|
|
base.setValue(val * -1);
|
|
caret = len - val.length;
|
|
base.last.start = p.start + caret;
|
|
base.last.end = p.end + caret;
|
|
$keyboard.caret(base.$preview, base.last);
|
|
base.setScroll();
|
|
}
|
|
},
|
|
space: function (base) {
|
|
base.insertText(' ');
|
|
},
|
|
tab: function (base) {
|
|
var tag = base.el.nodeName,
|
|
o = base.options;
|
|
if (tag !== 'TEXTAREA') {
|
|
if (o.tabNavigation) {
|
|
return base.switchInput(!base.shiftActive, true);
|
|
} else if (tag === 'INPUT') {
|
|
// ignore tab key in input
|
|
return false;
|
|
}
|
|
}
|
|
base.insertText('\t');
|
|
},
|
|
toggle: function (base) {
|
|
base.enabled = !base.enabled;
|
|
base.toggle();
|
|
},
|
|
// *** Special action keys: NBSP & zero-width characters ***
|
|
// Non-breaking space
|
|
NBSP: '\u00a0',
|
|
// zero width space
|
|
ZWSP: '\u200b',
|
|
// Zero width non-joiner
|
|
ZWNJ: '\u200c',
|
|
// Zero width joiner
|
|
ZWJ: '\u200d',
|
|
// Left-to-right Mark
|
|
LRM: '\u200e',
|
|
// Right-to-left Mark
|
|
RLM: '\u200f'
|
|
};
|
|
|
|
// Default keyboard layouts
|
|
$keyboard.builtLayouts = {};
|
|
$keyboard.layouts = {
|
|
'alpha': {
|
|
'normal': [
|
|
'` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
|
|
'{tab} a b c d e f g h i j [ ] \\',
|
|
'k l m n o p q r s ; \' {enter}',
|
|
'{shift} t u v w x y z , . / {shift}',
|
|
'{accept} {space} {cancel}'
|
|
],
|
|
'shift': [
|
|
'~ ! @ # $ % ^ & * ( ) _ + {bksp}',
|
|
'{tab} A B C D E F G H I J { } |',
|
|
'K L M N O P Q R S : " {enter}',
|
|
'{shift} T U V W X Y Z < > ? {shift}',
|
|
'{accept} {space} {cancel}'
|
|
]
|
|
},
|
|
'qwerty': {
|
|
'normal': [
|
|
'` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
|
|
'{tab} q w e r t y u i o p [ ] \\',
|
|
'a s d f g h j k l ; \' {enter}',
|
|
'{shift} z x c v b n m , . / {shift}',
|
|
'{accept} {space} {cancel}'
|
|
],
|
|
'shift': [
|
|
'~ ! @ # $ % ^ & * ( ) _ + {bksp}',
|
|
'{tab} Q W E R T Y U I O P { } |',
|
|
'A S D F G H J K L : " {enter}',
|
|
'{shift} Z X C V B N M < > ? {shift}',
|
|
'{accept} {space} {cancel}'
|
|
]
|
|
},
|
|
'international': {
|
|
'normal': [
|
|
'` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
|
|
'{tab} q w e r t y u i o p [ ] \\',
|
|
'a s d f g h j k l ; \' {enter}',
|
|
'{shift} z x c v b n m , . / {shift}',
|
|
'{accept} {alt} {space} {alt} {cancel}'
|
|
],
|
|
'shift': [
|
|
'~ ! @ # $ % ^ & * ( ) _ + {bksp}',
|
|
'{tab} Q W E R T Y U I O P { } |',
|
|
'A S D F G H J K L : " {enter}',
|
|
'{shift} Z X C V B N M < > ? {shift}',
|
|
'{accept} {alt} {space} {alt} {cancel}'
|
|
],
|
|
'alt': [
|
|
'~ \u00a1 \u00b2 \u00b3 \u00a4 \u20ac \u00bc \u00bd \u00be \u2018 \u2019 \u00a5 \u00d7 {bksp}',
|
|
'{tab} \u00e4 \u00e5 \u00e9 \u00ae \u00fe \u00fc \u00fa \u00ed \u00f3 \u00f6 \u00ab \u00bb \u00ac',
|
|
'\u00e1 \u00df \u00f0 f g h j k \u00f8 \u00b6 \u00b4 {enter}',
|
|
'{shift} \u00e6 x \u00a9 v b \u00f1 \u00b5 \u00e7 > \u00bf {shift}',
|
|
'{accept} {alt} {space} {alt} {cancel}'
|
|
],
|
|
'alt-shift': [
|
|
'~ \u00b9 \u00b2 \u00b3 \u00a3 \u20ac \u00bc \u00bd \u00be \u2018 \u2019 \u00a5 \u00f7 {bksp}',
|
|
'{tab} \u00c4 \u00c5 \u00c9 \u00ae \u00de \u00dc \u00da \u00cd \u00d3 \u00d6 \u00ab \u00bb \u00a6',
|
|
'\u00c4 \u00a7 \u00d0 F G H J K \u00d8 \u00b0 \u00a8 {enter}',
|
|
'{shift} \u00c6 X \u00a2 V B \u00d1 \u00b5 \u00c7 . \u00bf {shift}',
|
|
'{accept} {alt} {space} {alt} {cancel}'
|
|
]
|
|
},
|
|
'colemak': {
|
|
'normal': [
|
|
'` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
|
|
'{tab} q w f p g j l u y ; [ ] \\',
|
|
'{bksp} a r s t d h n e i o \' {enter}',
|
|
'{shift} z x c v b k m , . / {shift}',
|
|
'{accept} {space} {cancel}'
|
|
],
|
|
'shift': [
|
|
'~ ! @ # $ % ^ & * ( ) _ + {bksp}',
|
|
'{tab} Q W F P G J L U Y : { } |',
|
|
'{bksp} A R S T D H N E I O " {enter}',
|
|
'{shift} Z X C V B K M < > ? {shift}',
|
|
'{accept} {space} {cancel}'
|
|
]
|
|
},
|
|
'dvorak': {
|
|
'normal': [
|
|
'` 1 2 3 4 5 6 7 8 9 0 [ ] {bksp}',
|
|
'{tab} \' , . p y f g c r l / = \\',
|
|
'a o e u i d h t n s - {enter}',
|
|
'{shift} ; q j k x b m w v z {shift}',
|
|
'{accept} {space} {cancel}'
|
|
],
|
|
'shift': [
|
|
'~ ! @ # $ % ^ & * ( ) { } {bksp}',
|
|
'{tab} " < > P Y F G C R L ? + |',
|
|
'A O E U I D H T N S _ {enter}',
|
|
'{shift} : Q J K X B M W V Z {shift}',
|
|
'{accept} {space} {cancel}'
|
|
]
|
|
},
|
|
'num': {
|
|
'normal': [
|
|
'= ( ) {b}',
|
|
'{clear} / * -',
|
|
'7 8 9 +',
|
|
'4 5 6 {sign}',
|
|
'1 2 3 %',
|
|
'0 {dec} {a} {c}'
|
|
]
|
|
}
|
|
};
|
|
|
|
$keyboard.language = {
|
|
en: {
|
|
display: {
|
|
// check mark - same action as accept
|
|
'a': '\u2714:Accept (Shift+Enter)',
|
|
'accept': 'Accept:Accept (Shift+Enter)',
|
|
// other alternatives \u2311
|
|
'alt': 'Alt:\u2325 AltGr',
|
|
// Left arrow (same as ←)
|
|
'b': '\u232b:Backspace',
|
|
'bksp': 'Bksp:Backspace',
|
|
// big X, close - same action as cancel
|
|
'c': '\u2716:Cancel (Esc)',
|
|
'cancel': 'Cancel:Cancel (Esc)',
|
|
// clear num pad
|
|
'clear': 'C:Clear',
|
|
'combo': '\u00f6:Toggle Combo Keys',
|
|
// decimal point for num pad (optional), change '.' to ',' for European format
|
|
'dec': '.:Decimal',
|
|
// down, then left arrow - enter symbol
|
|
'e': '\u23ce:Enter',
|
|
'empty': '\u00a0',
|
|
'enter': 'Enter:Enter \u23ce',
|
|
// left arrow (move caret)
|
|
'left': '\u2190',
|
|
// caps lock
|
|
'lock': 'Lock:\u21ea Caps Lock',
|
|
'next': 'Next \u21e8',
|
|
'prev': '\u21e6 Prev',
|
|
// right arrow (move caret)
|
|
'right': '\u2192',
|
|
// thick hollow up arrow
|
|
's': '\u21e7:Shift',
|
|
'shift': 'Shift:Shift',
|
|
// +/- sign for num pad
|
|
'sign': '\u00b1:Change Sign',
|
|
'space': '\u00a0:Space',
|
|
// right arrow to bar (used since this virtual keyboard works with one directional tabs)
|
|
't': '\u21e5:Tab',
|
|
// \u21b9 is the true tab symbol (left & right arrows)
|
|
'tab': '\u21e5 Tab:Tab',
|
|
// replaced by an image
|
|
'toggle': ' ',
|
|
|
|
// added to titles of keys
|
|
// accept key status when acceptValid:true
|
|
'valid': 'valid',
|
|
'invalid': 'invalid',
|
|
// combo key states
|
|
'active': 'active',
|
|
'disabled': 'disabled'
|
|
},
|
|
|
|
// Message added to the key title while hovering, if the mousewheel plugin exists
|
|
wheelMessage: 'Use mousewheel to see other keys',
|
|
|
|
comboRegex: /([`\'~\^\"ao])([a-z])/mig,
|
|
combos: {
|
|
// grave
|
|
'`': { a: '\u00e0', A: '\u00c0', e: '\u00e8', E: '\u00c8', i: '\u00ec', I: '\u00cc', o: '\u00f2',
|
|
O: '\u00d2', u: '\u00f9', U: '\u00d9', y: '\u1ef3', Y: '\u1ef2' },
|
|
// acute & cedilla
|
|
"'": { a: '\u00e1', A: '\u00c1', e: '\u00e9', E: '\u00c9', i: '\u00ed', I: '\u00cd', o: '\u00f3',
|
|
O: '\u00d3', u: '\u00fa', U: '\u00da', y: '\u00fd', Y: '\u00dd' },
|
|
// umlaut/trema
|
|
'"': { a: '\u00e4', A: '\u00c4', e: '\u00eb', E: '\u00cb', i: '\u00ef', I: '\u00cf', o: '\u00f6',
|
|
O: '\u00d6', u: '\u00fc', U: '\u00dc', y: '\u00ff', Y: '\u0178' },
|
|
// circumflex
|
|
'^': { a: '\u00e2', A: '\u00c2', e: '\u00ea', E: '\u00ca', i: '\u00ee', I: '\u00ce', o: '\u00f4',
|
|
O: '\u00d4', u: '\u00fb', U: '\u00db', y: '\u0177', Y: '\u0176' },
|
|
// tilde
|
|
'~': { a: '\u00e3', A: '\u00c3', e: '\u1ebd', E: '\u1ebc', i: '\u0129', I: '\u0128', o: '\u00f5',
|
|
O: '\u00d5', u: '\u0169', U: '\u0168', y: '\u1ef9', Y: '\u1ef8', n: '\u00f1', N: '\u00d1' }
|
|
}
|
|
}
|
|
};
|
|
|
|
$keyboard.defaultOptions = {
|
|
// set this to ISO 639-1 language code to override language set by the layout
|
|
// http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
|
// language defaults to 'en' if not found
|
|
language: null,
|
|
rtl: false,
|
|
|
|
// *** choose layout & positioning ***
|
|
layout: 'qwerty',
|
|
customLayout: null,
|
|
|
|
position: {
|
|
// optional - null (attach to input/textarea) or a jQuery object (attach elsewhere)
|
|
of: null,
|
|
my: 'center top',
|
|
at: 'center top',
|
|
// used when 'usePreview' is false (centers the keyboard at the bottom of the input/textarea)
|
|
at2: 'center bottom'
|
|
},
|
|
|
|
// allow jQuery position utility to reposition the keyboard on window resize
|
|
reposition: true,
|
|
|
|
// preview added above keyboard if true, original input/textarea used if false
|
|
usePreview: true,
|
|
|
|
// if true, the keyboard will always be visible
|
|
alwaysOpen: false,
|
|
|
|
// give the preview initial focus when the keyboard becomes visible
|
|
initialFocus: true,
|
|
|
|
// avoid changing the focus (hardware keyboard probably won't work)
|
|
noFocus: false,
|
|
|
|
// if true, keyboard will remain open even if the input loses focus, but closes on escape
|
|
// or when another keyboard opens.
|
|
stayOpen: false,
|
|
|
|
// Prevents the keyboard from closing when the user clicks or presses outside the keyboard
|
|
// the `autoAccept` option must also be set to true when this option is true or changes are lost
|
|
userClosed: false,
|
|
|
|
// if true, keyboard will not close if you press escape.
|
|
ignoreEsc: false,
|
|
|
|
// if true, keyboard will only closed on click event instead of mousedown and touchstart
|
|
closeByClickEvent: false,
|
|
|
|
css: {
|
|
// input & preview
|
|
input: 'ui-widget-content ui-corner-all',
|
|
// keyboard container
|
|
container: 'ui-widget-content ui-widget ui-corner-all ui-helper-clearfix',
|
|
// keyboard container extra class (same as container, but separate)
|
|
popup: '',
|
|
// default state
|
|
buttonDefault: 'ui-state-default ui-corner-all',
|
|
// hovered button
|
|
buttonHover: 'ui-state-hover',
|
|
// Action keys (e.g. Accept, Cancel, Tab, etc); this replaces 'actionClass' option
|
|
buttonAction: 'ui-state-active',
|
|
// Active keys (e.g. shift down, meta keyset active, combo keys active)
|
|
buttonActive: 'ui-state-active',
|
|
// used when disabling the decimal button {dec} when a decimal exists in the input area
|
|
buttonDisabled: 'ui-state-disabled',
|
|
buttonEmpty: 'ui-keyboard-empty'
|
|
},
|
|
|
|
// *** Useability ***
|
|
// Auto-accept content when clicking outside the keyboard (popup will close)
|
|
autoAccept: false,
|
|
// Auto-accept content even if the user presses escape (only works if `autoAccept` is `true`)
|
|
autoAcceptOnEsc: false,
|
|
|
|
// Prevents direct input in the preview window when true
|
|
lockInput: false,
|
|
|
|
// Prevent keys not in the displayed keyboard from being typed in
|
|
restrictInput: false,
|
|
// Additional allowed characters while restrictInput is true
|
|
restrictInclude: '', // e.g. 'a b foo \ud83d\ude38'
|
|
|
|
// Check input against validate function, if valid the accept button gets a class name of
|
|
// 'ui-keyboard-valid-input'. If invalid, the accept button gets a class name of
|
|
// 'ui-keyboard-invalid-input'
|
|
acceptValid: false,
|
|
// Auto-accept when input is valid; requires `acceptValid` set `true` & validate callback
|
|
autoAcceptOnValid: false,
|
|
|
|
// if acceptValid is true & the validate function returns a false, this option will cancel
|
|
// a keyboard close only after the accept button is pressed
|
|
cancelClose: true,
|
|
|
|
// tab to go to next, shift-tab for previous (default behavior)
|
|
tabNavigation: false,
|
|
|
|
// enter for next input; shift+enter accepts content & goes to next
|
|
// shift + 'enterMod' + enter ('enterMod' is the alt as set below) will accept content and go
|
|
// to previous in a textarea
|
|
enterNavigation: false,
|
|
// mod key options: 'ctrlKey', 'shiftKey', 'altKey', 'metaKey' (MAC only)
|
|
enterMod: 'altKey', // alt-enter to go to previous; shift-alt-enter to accept & go to previous
|
|
|
|
// if true, the next button will stop on the last keyboard input/textarea; prev button stops at first
|
|
// if false, the next button will wrap to target the first input/textarea; prev will go to the last
|
|
stopAtEnd: true,
|
|
|
|
// Set this to append the keyboard after the input/textarea (appended to the input/textarea parent).
|
|
// This option works best when the input container doesn't have a set width & when the 'tabNavigation'
|
|
// option is true.
|
|
appendLocally: false,
|
|
// When appendLocally is false, the keyboard will be appended to this object
|
|
appendTo: 'body',
|
|
|
|
// Wrap all <br>s inside of a contenteditable in a div; without wrapping, the caret
|
|
// position will not be accurate
|
|
wrapBRs: true,
|
|
|
|
// If false, the shift key will remain active until the next key is (mouse) clicked on; if true it will
|
|
// stay active until pressed again
|
|
stickyShift: true,
|
|
|
|
// Prevent pasting content into the area
|
|
preventPaste: false,
|
|
|
|
// caret placed at the end of any text when keyboard becomes visible
|
|
caretToEnd: false,
|
|
|
|
// caret stays this many pixels from the edge of the input while scrolling left/right;
|
|
// use "c" or "center" to center the caret while scrolling
|
|
scrollAdjustment: 10,
|
|
|
|
// Set the max number of characters allowed in the input, setting it to false disables this option
|
|
maxLength: false,
|
|
// allow inserting characters @ caret when maxLength is set
|
|
maxInsert: true,
|
|
|
|
// Mouse repeat delay - when clicking/touching a virtual keyboard key, after this delay the key will
|
|
// start repeating
|
|
repeatDelay: 500,
|
|
|
|
// Mouse repeat rate - after the repeatDelay, this is the rate (characters per second) at which the
|
|
// key is repeated Added to simulate holding down a real keyboard key and having it repeat. I haven't
|
|
// calculated the upper limit of this rate, but it is limited to how fast the javascript can process
|
|
// the keys. And for me, in Firefox, it's around 20.
|
|
repeatRate: 20,
|
|
|
|
// resets the keyboard to the default keyset when visible
|
|
resetDefault: true,
|
|
|
|
// Event (namespaced) on the input to reveal the keyboard. To disable it, just set it to ''.
|
|
openOn: 'focus',
|
|
|
|
// Event (namepaced) for when the character is added to the input (clicking on the keyboard)
|
|
keyBinding: 'mousedown touchstart',
|
|
|
|
// enable/disable mousewheel functionality
|
|
// enabling still depends on the mousewheel plugin
|
|
useWheel: true,
|
|
|
|
// combos (emulate dead keys : http://en.wikipedia.org/wiki/Keyboard_layout#US-International)
|
|
// if user inputs `a the script converts it to à, ^o becomes ô, etc.
|
|
useCombos: true,
|
|
|
|
/*
|
|
// *** Methods ***
|
|
// commenting these out to reduce the size of the minified version
|
|
// Callbacks - attach a function to any of these callbacks as desired
|
|
initialized : function(e, keyboard, el) {},
|
|
beforeVisible : function(e, keyboard, el) {},
|
|
visible : function(e, keyboard, el) {},
|
|
beforeInsert : function(e, keyboard, el, textToAdd) { return textToAdd; },
|
|
change : function(e, keyboard, el) {},
|
|
beforeClose : function(e, keyboard, el, accepted) {},
|
|
accepted : function(e, keyboard, el) {},
|
|
canceled : function(e, keyboard, el) {},
|
|
restricted : function(e, keyboard, el) {},
|
|
hidden : function(e, keyboard, el) {},
|
|
// called instead of base.switchInput
|
|
switchInput : function(keyboard, goToNext, isAccepted) {},
|
|
// used if you want to create a custom layout or modify the built-in keyboard
|
|
create : function(keyboard) { return keyboard.buildKeyboard(); },
|
|
|
|
// build key callback
|
|
buildKey : function( keyboard, data ) {
|
|
/ *
|
|
data = {
|
|
// READ ONLY
|
|
isAction : [boolean] true if key is an action key
|
|
name : [string] key class name suffix ( prefix = 'ui-keyboard-' );
|
|
may include decimal ascii value of character
|
|
value : [string] text inserted (non-action keys)
|
|
title : [string] title attribute of key
|
|
action : [string] keyaction name
|
|
html : [string] HTML of the key; it includes a <span> wrapping the text
|
|
// use to modify key HTML
|
|
$key : [object] jQuery selector of key which is already appended to keyboard
|
|
}
|
|
* /
|
|
return data;
|
|
},
|
|
*/
|
|
|
|
// this callback is called, if the acceptValid is true, and just before the 'beforeClose' to check
|
|
// the value if the value is valid, return true and the keyboard will continue as it should
|
|
// (close if not always open, etc). If the value is not valid, return false and clear the keyboard
|
|
// value ( like this "keyboard.$preview.val('');" ), if desired. The validate function is called after
|
|
// each input, the 'isClosing' value will be false; when the accept button is clicked,
|
|
// 'isClosing' is true
|
|
validate: function (keyboard, value, isClosing) {
|
|
return true;
|
|
}
|
|
|
|
};
|
|
|
|
// for checking combos
|
|
$keyboard.comboRegex = /([`\'~\^\"ao])([a-z])/mig;
|
|
|
|
// store current keyboard element; used by base.isCurrent()
|
|
$keyboard.currentKeyboard = '';
|
|
|
|
$('<!--[if lte IE 8]><script>jQuery("body").addClass("oldie");</script><![endif]--><!--[if IE]>' +
|
|
'<script>jQuery("body").addClass("ie");</script><![endif]-->')
|
|
.appendTo('body')
|
|
.remove();
|
|
$keyboard.msie = $('body').hasClass('oldie'); // Old IE flag, used for caret positioning
|
|
$keyboard.allie = $('body').hasClass('ie');
|
|
|
|
$keyboard.watermark = (typeof (document.createElement('input').placeholder) !== 'undefined');
|
|
|
|
$keyboard.checkCaretSupport = function () {
|
|
if (typeof $keyboard.checkCaret !== 'boolean') {
|
|
// Check if caret position is saved when input is hidden or loses focus
|
|
// (*cough* all versions of IE and I think Opera has/had an issue as well
|
|
var $temp = $('<div style="height:0px;width:0px;overflow:hidden;position:fixed;top:0;left:-100px;">' +
|
|
'<input type="text" value="testing"/></div>').prependTo('body'); // stop page scrolling
|
|
$keyboard.caret($temp.find('input'), 3, 3);
|
|
// Also save caret position of the input if it is locked
|
|
$keyboard.checkCaret = $keyboard.caret($temp.find('input').hide().show()).start !== 3;
|
|
$temp.remove();
|
|
}
|
|
return $keyboard.checkCaret;
|
|
};
|
|
|
|
$keyboard.caret = function($el, param1, param2) {
|
|
if (!$el.length || $el.is(':hidden') || $el.css('visibility') === 'hidden') {
|
|
return {};
|
|
}
|
|
var start, end, txt, pos,
|
|
kb = $el.data( 'keyboard' ),
|
|
noFocus = kb && kb.options.noFocus,
|
|
formEl = /(textarea|input)/i.test($el[0].nodeName);
|
|
if (!noFocus) { $el.focus(); }
|
|
// set caret position
|
|
if (typeof param1 !== 'undefined') {
|
|
// allow setting caret using ( $el, { start: x, end: y } )
|
|
if (typeof param1 === 'object' && 'start' in param1 && 'end' in param1) {
|
|
start = param1.start;
|
|
end = param1.end;
|
|
} else if (typeof param2 === 'undefined') {
|
|
param2 = param1; // set caret using start position
|
|
}
|
|
// set caret using ( $el, start, end );
|
|
if (typeof param1 === 'number' && typeof param2 === 'number') {
|
|
start = param1;
|
|
end = param2;
|
|
} else if ( param1 === 'start' ) {
|
|
start = end = 0;
|
|
} else if ( typeof param1 === 'string' ) {
|
|
// unknown string setting, move caret to end
|
|
start = end = 'end';
|
|
}
|
|
|
|
// *** SET CARET POSITION ***
|
|
// modify the line below to adapt to other caret plugins
|
|
return formEl ?
|
|
$el.caret( start, end, noFocus ) :
|
|
$keyboard.setEditableCaret( $el, start, end );
|
|
}
|
|
// *** GET CARET POSITION ***
|
|
// modify the line below to adapt to other caret plugins
|
|
if (formEl) {
|
|
// modify the line below to adapt to other caret plugins
|
|
pos = $el.caret();
|
|
} else {
|
|
// contenteditable
|
|
pos = $keyboard.getEditableCaret($el[0]);
|
|
}
|
|
start = pos.start;
|
|
end = pos.end;
|
|
|
|
// *** utilities ***
|
|
txt = formEl && $el[0].value || $el.text() || '';
|
|
return {
|
|
start : start,
|
|
end : end,
|
|
// return selected text
|
|
text : txt.substring( start, end ),
|
|
// return a replace selected string method
|
|
replaceStr : function( str ) {
|
|
return txt.substring( 0, start ) + str + txt.substring( end, txt.length );
|
|
}
|
|
};
|
|
};
|
|
|
|
$keyboard.isTextNode = function(el) {
|
|
return el && el.nodeType === 3;
|
|
};
|
|
|
|
$keyboard.isBlock = function(el, node) {
|
|
var win = el.ownerDocument.defaultView;
|
|
if (
|
|
node && node.nodeType === 1 && node !== el &&
|
|
win.getComputedStyle(node).display === 'block'
|
|
) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
// Wrap all BR's inside of contenteditable
|
|
$keyboard.wrapBRs = function(container) {
|
|
var $el = $(container).find('br:not(.' + $keyboard.css.divWrapperCE + ')');
|
|
if ($el.length) {
|
|
$.each($el, function(i, el) {
|
|
var len = el.parentNode.childNodes.length;
|
|
if (
|
|
// wrap BRs if not solo child
|
|
len !== 1 ||
|
|
// Or if BR is wrapped by a span
|
|
len === 1 && !$keyboard.isBlock(container, el.parentNode)
|
|
) {
|
|
$(el).addClass($keyboard.css.divWrapperCE).wrap('<div>');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
$keyboard.getEditableCaret = function(container) {
|
|
container = $(container)[0];
|
|
if (!container.isContentEditable) { return {}; }
|
|
var end, text,
|
|
options = ($(container).data('keyboard') || {}).options,
|
|
doc = container.ownerDocument,
|
|
range = doc.getSelection().getRangeAt(0),
|
|
result = pathToNode(range.startContainer, range.startOffset),
|
|
start = result.position;
|
|
if (options.wrapBRs !== false) {
|
|
$keyboard.wrapBRs(container);
|
|
}
|
|
function pathToNode(endNode, offset) {
|
|
var node, adjust,
|
|
txt = '',
|
|
done = false,
|
|
position = 0,
|
|
nodes = $.makeArray(container.childNodes);
|
|
|
|
function checkBlock(val) {
|
|
if (val) {
|
|
position += val;
|
|
txt += options && options.replaceCR || '\n';
|
|
}
|
|
}
|
|
|
|
while (!done && nodes.length) {
|
|
node = nodes.shift();
|
|
if (node === endNode) {
|
|
done = true;
|
|
}
|
|
|
|
// Add one if previous sibling was a block node (div, p, etc)
|
|
adjust = $keyboard.isBlock(container, node.previousSibling);
|
|
checkBlock(adjust);
|
|
|
|
if ($keyboard.isTextNode(node)) {
|
|
position += done ? offset : node.length;
|
|
txt += node.textContent;
|
|
if (done) {
|
|
return {position: position, text: txt};
|
|
}
|
|
} else if (!done && node.childNodes) {
|
|
nodes = $.makeArray(node.childNodes).concat(nodes);
|
|
}
|
|
// Add one if we're inside a block node (div, p, etc)
|
|
// and previous sibling was a text node
|
|
adjust = $keyboard.isTextNode(node.previousSibling) && $keyboard.isBlock(container, node);
|
|
checkBlock(adjust);
|
|
}
|
|
return {position: position, text: txt};
|
|
}
|
|
// check of start and end are the same
|
|
if (range.endContainer === range.startContainer && range.endOffset === range.startOffset) {
|
|
end = start;
|
|
text = '';
|
|
} else {
|
|
result = pathToNode(range.endContainer, range.endOffset);
|
|
end = result.position;
|
|
text = result.text.substring(start, end);
|
|
}
|
|
return {
|
|
start: start,
|
|
end: end,
|
|
text: text
|
|
};
|
|
};
|
|
|
|
$keyboard.getEditableLength = function(container) {
|
|
var result = $keyboard.setEditableCaret(container, 'getMax');
|
|
// if not a number, the container is not a contenteditable element
|
|
return typeof result === 'number' ? result : null;
|
|
};
|
|
|
|
$keyboard.setEditableCaret = function(container, start, end) {
|
|
container = $(container)[0];
|
|
if (!container.isContentEditable) { return {}; }
|
|
var doc = container.ownerDocument,
|
|
range = doc.createRange(),
|
|
sel = doc.getSelection(),
|
|
options = ($(container).data('keyboard') || {}).options,
|
|
s = start,
|
|
e = end,
|
|
text = '',
|
|
result = findNode(start === 'getMax' ? 'end' : start);
|
|
function findNode(offset) {
|
|
if (offset === 'end') {
|
|
// Set some value > content length; but return max
|
|
offset = container.innerHTML.length;
|
|
} else if (offset < 0) {
|
|
offset = 0;
|
|
}
|
|
var node, check,
|
|
txt = '',
|
|
done = false,
|
|
position = 0,
|
|
last = 0,
|
|
max = 0,
|
|
nodes = $.makeArray(container.childNodes);
|
|
function updateText(val) {
|
|
txt += val ? options && options.replaceCR || "\n" : "";
|
|
return val > 0;
|
|
}
|
|
function checkDone(adj) {
|
|
var val = position + adj;
|
|
last = max;
|
|
max += adj;
|
|
if (offset - val >= 0) {
|
|
position = val;
|
|
return offset - position <= 0;
|
|
}
|
|
return offset - val <= 0;
|
|
}
|
|
while (!done && nodes.length) {
|
|
node = nodes.shift();
|
|
// Add one if the previous sibling was a block node (div, p, etc)
|
|
check = $keyboard.isBlock(container, node.previousSibling);
|
|
if (updateText(check) && checkDone(check)) {
|
|
done = true;
|
|
}
|
|
// Add one if we're inside a block node (div, p, etc)
|
|
check = $keyboard.isTextNode(node.previousSibling) && $keyboard.isBlock(container, node);
|
|
if (updateText(check) && checkDone(check)) {
|
|
done = true;
|
|
}
|
|
if ($keyboard.isTextNode(node)) {
|
|
txt += node.textContent;
|
|
if (checkDone(node.length)) {
|
|
check = offset - position === 0 && position - last >= 1 ? node.length : offset - position;
|
|
return {
|
|
node: node,
|
|
offset: check,
|
|
position: offset,
|
|
text: txt
|
|
};
|
|
}
|
|
} else if (!done && node.childNodes) {
|
|
nodes = $.makeArray(node.childNodes).concat(nodes);
|
|
}
|
|
}
|
|
return nodes.length ?
|
|
{node: node, offset: offset - position, position: offset, text: txt} :
|
|
// Offset is larger than content, return max
|
|
{node: node, offset: node && node.length || 0, position: max, text: txt};
|
|
}
|
|
if (result.node) {
|
|
s = result.position; // Adjust if start > content length
|
|
if (start === 'getMax') {
|
|
return s;
|
|
}
|
|
range.setStart(result.node, result.offset);
|
|
// Only find end if > start and is defined... this allows passing
|
|
// setEditableCaret(el, 'end') or setEditableCaret(el, 10, 'end');
|
|
if (typeof end !== "undefined" && end !== start) {
|
|
result = findNode(end);
|
|
}
|
|
if (result.node) {
|
|
e = result.position; // Adjust if end > content length
|
|
range.setEnd(result.node, result.offset);
|
|
text = s === e ? "" : result.text.substring(s, e);
|
|
}
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
}
|
|
return {
|
|
start: s,
|
|
end: e,
|
|
text: text
|
|
};
|
|
};
|
|
|
|
$keyboard.replaceContent = function (el, param) {
|
|
el = $(el)[0];
|
|
var node, i, str,
|
|
type = typeof param,
|
|
caret = $keyboard.getEditableCaret(el).start,
|
|
charIndex = 0,
|
|
nodeStack = [el];
|
|
while ((node = nodeStack.pop())) {
|
|
if ($keyboard.isTextNode(node)) {
|
|
if (type === 'function') {
|
|
if (caret >= charIndex && caret <= charIndex + node.length) {
|
|
node.textContent = param(node.textContent);
|
|
}
|
|
} else if (type === 'string') {
|
|
// maybe not the best method, but it works for simple changes
|
|
str = param.substring(charIndex, charIndex + node.length);
|
|
if (str !== node.textContent) {
|
|
node.textContent = str;
|
|
}
|
|
}
|
|
charIndex += node.length;
|
|
} else if (node && node.childNodes) {
|
|
i = node.childNodes.length;
|
|
while (i--) {
|
|
nodeStack.push(node.childNodes[i]);
|
|
}
|
|
}
|
|
}
|
|
i = $keyboard.getEditableCaret(el);
|
|
$keyboard.setEditableCaret(el, i.start, i.start);
|
|
};
|
|
|
|
$.fn.keyboard = function (options) {
|
|
return this.each(function () {
|
|
if (!$(this).data('keyboard')) {
|
|
/*jshint nonew:false */
|
|
(new $.keyboard(this, options));
|
|
}
|
|
});
|
|
};
|
|
|
|
$.fn.getkeyboard = function () {
|
|
return this.data('keyboard');
|
|
};
|
|
|
|
/* Copyright (c) 2010 C. F., Wong (<a href="http://cloudgen.w0ng.hk">Cloudgen Examplet Store</a>)
|
|
* Licensed under the MIT License:
|
|
* http://www.opensource.org/licenses/mit-license.php
|
|
* Highly modified from the original
|
|
*/
|
|
|
|
$.fn.caret = function (start, end, noFocus) {
|
|
if (
|
|
typeof this[0] === 'undefined' ||
|
|
this.is(':hidden') ||
|
|
this.css('visibility') === 'hidden' ||
|
|
!/(INPUT|TEXTAREA)/.test(this[0].nodeName)
|
|
) {
|
|
return this;
|
|
}
|
|
var selRange, range, stored_range, txt, val,
|
|
$el = this,
|
|
el = $el[0],
|
|
selection = el.ownerDocument.selection,
|
|
sTop = el.scrollTop,
|
|
ss = false,
|
|
supportCaret = true;
|
|
try {
|
|
ss = 'selectionStart' in el;
|
|
} catch (err) {
|
|
supportCaret = false;
|
|
}
|
|
if (supportCaret && typeof start !== 'undefined') {
|
|
if (!/(email|number)/i.test(el.type)) {
|
|
if (ss) {
|
|
el.selectionStart = start;
|
|
el.selectionEnd = end;
|
|
} else {
|
|
selRange = el.createTextRange();
|
|
selRange.collapse(true);
|
|
selRange.moveStart('character', start);
|
|
selRange.moveEnd('character', end - start);
|
|
selRange.select();
|
|
}
|
|
}
|
|
// must be visible or IE8 crashes; IE9 in compatibility mode works fine - issue #56
|
|
if (!noFocus && ($el.is(':visible') || $el.css('visibility') !== 'hidden')) {
|
|
el.focus();
|
|
}
|
|
el.scrollTop = sTop;
|
|
return this;
|
|
} else {
|
|
if (/(email|number)/i.test(el.type)) {
|
|
// fix suggested by raduanastase (https://github.com/Mottie/Keyboard/issues/105#issuecomment-40456535)
|
|
start = end = $el.val().length;
|
|
} else if (ss) {
|
|
start = el.selectionStart;
|
|
end = el.selectionEnd;
|
|
} else if (selection) {
|
|
if (el.nodeName === 'TEXTAREA') {
|
|
val = $el.val();
|
|
range = selection.createRange();
|
|
stored_range = range.duplicate();
|
|
stored_range.moveToElementText(el);
|
|
stored_range.setEndPoint('EndToEnd', range);
|
|
// thanks to the awesome comments in the rangy plugin
|
|
start = stored_range.text.replace(/\r/g, '\n').length;
|
|
end = start + range.text.replace(/\r/g, '\n').length;
|
|
} else {
|
|
val = $el.val().replace(/\r/g, '\n');
|
|
range = selection.createRange().duplicate();
|
|
range.moveEnd('character', val.length);
|
|
start = (range.text === '' ? val.length : val.lastIndexOf(range.text));
|
|
range = selection.createRange().duplicate();
|
|
range.moveStart('character', -val.length);
|
|
end = range.text.length;
|
|
}
|
|
} else {
|
|
// caret positioning not supported
|
|
start = end = (el.value || '').length;
|
|
}
|
|
txt = (el.value || '');
|
|
return {
|
|
start: start,
|
|
end: end,
|
|
text: txt.substring(start, end),
|
|
replace: function (str) {
|
|
return txt.substring(0, start) + str + txt.substring(end, txt.length);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
return $keyboard;
|
|
|
|
}));
|