From c74806bd1b19fb860488d2c18cb9afe2621faaa4 Mon Sep 17 00:00:00 2001 From: Andy Earnshaw Date: Mon, 16 Jun 2014 23:46:31 +0100 Subject: [PATCH] Initial commit --- LICENSE.txt | 21 ++ README.md | 39 +++ bower.json | 12 + demo.html | 12 + keylime.js | 772 +++++++++++++++++++++++++++++++++++++++++++++++++ keylime.min.js | 1 + 6 files changed, 857 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 bower.json create mode 100644 demo.html create mode 100644 keylime.js create mode 100644 keylime.min.js diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..85240c2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Andy Earnshaw + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb3bd65 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Key Lime - JavaScript-Based Input Method Editor + +Key Lime is an input method editor written using pure JavaScript. The library +is designed for Smart TV applications where the manufacturers' virtual +keyboards can be awkward use. For example, Samsung's JavaScript IME needs to +have an instance individually attached to every input element you want to use +it for, and LG Smart TV's native virtual keyboard is great but there's no way +to detect when the user closes it. So, in the spirit of writing less code, Key +Lime allows you to just add the script and "fuggedaboudit". + +## Getting started + +Download the JavaScript source and add it to your application's HTML: + + + +Alternatively, install with Bower: + + bower install keylime + +That's it. The IME will auto-appear on focus of a writable text input. +You can change this to work manually by setting + + keyLime.config.noauto = true; + +Then just call `keyLime.show()` or `keyLime.hide()` whenever you need it. + +## Roadmap + + - Make it easy to add other languages + - Allow certain keys to be disabled for different input types + - Add other types of input methods, like numeric only, and perhaps date/time + - Add dictionary and suggestions support + +## License + +Copyright (c) 2014 Andy Earnshaw + +This software is licensed under the MIT license. See the LICENSE.txt file accompanying this software for terms of use. diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..1fccb66 --- /dev/null +++ b/bower.json @@ -0,0 +1,12 @@ +{ + "name": "keylime", + "version": "0.0.1", + "description": "Key Lime is an input method editor written using pure JavaScript.", + "main": ["keylime.js", "keylime.min.js"], + "keywords": [ + "keyboard", + "ime", + "input method editor" + ] +} + diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..32a98d9 --- /dev/null +++ b/demo.html @@ -0,0 +1,12 @@ + + + + + Key Lime Demo + + + +

This is a demo page. Focus the following input element and play around with the keyboard.

+ + + diff --git a/keylime.js b/keylime.js new file mode 100644 index 0000000..5a53837 --- /dev/null +++ b/keylime.js @@ -0,0 +1,772 @@ +/*jshint laxbreak:true, boss:true, shadow:true*/ +/** + * @license Copyright 2014 Andy Earnshaw, MIT License + * + * A simple input method editor written in JavaScript and CSS + */ +(function (global, factory) { + var exported, + keyLime = factory(global); + + if (typeof define === 'function' && define.amd) + define(exported = keyLime); + + if (typeof exports === 'object') + module.exports = exported = keyLime; + + global.keyLime = keyLime; + +})(typeof global !== 'undefined' ? global : this, function (global) { +"use strict"; + +var + holdTimer, focused, shift, caps, sym, move, lastLeft, ruleIdx, diacriticsMenu, + style = document.createElement('style'), + imeCtr = document.createElement('div'), + exports = global.keyLime || { config: {} }, + visible = false, + + // Didn't seem worth having a separate file for the default styles, so here they are + cssRules = [ + '.lime-container { background-color: #333; position: absolute; bottom: 0; left: 0; right: 0; color: #fff; z-index:1000000; }', + '.lime-container-dim::before { position: absolute; content: ""; top: 0; left: 0; right: 0; bottom: 0; background-color: #000; opacity: 0.5; }', + '.lime-key-row { list-style-type: none; clear: both; text-align: center; padding: 0; margin: 0; font-size: 28px; font-weight: bold; }', + '.lime-diacritics-row { position: absolute; z-index: 2; overflow: hidden; -webkit-transition: width 400ms; transition: width 400ms; white-space: nowrap; }', + '.lime-key { vertical-align: top; display: inline-block; border: 3px solid #333; background-color: #666; width: 66px; line-height: 50px; -webkit-transition: all 400ms linear; transition: all 400ms linear; }', + + '.lime-key[data-text]::before { content: attr(data-text) }', + '.lime-container.symbol-toggle .lime-key[data-symbol]::before { content: attr(data-symbol) }', + '.lime-container.shift-toggle .lime-key[data-text]:not(.lime-http):not(.lime-dotcom):not(.lime-wwwdot)::before { text-transform: uppercase; }', + '.lime-special-key { background-color: #999; color: #333; }', + '.lime-return { background-color: #ccc; width: 138px; }', + '.lime-focus { background-color: #26f; color: #fff; }', + '.lime-toggle { background-color: #2f2; color: #fff; position: relative; }', + '.lime-spacebar { width:498px; }', + '.lime-http, .lime-dotcom, .lime-wwwdot { font-size: 20px; }' + ], + + // elements that definitely won't throw an error for .selection[Start|End] + regSupportSel = /^(?:text|search|url|tel|password)$/i, + supportAnySel = wontThrowOnSelection(), + + // Special/Modifier keys + spKeys = { + Tab: tabNext, + Return: submit, + + /** + * Switches to uppercase mode + */ + Shift: function (f) { + var t = imeCtr.querySelector('.lime-toggle'); + + if (t) + t.classList.remove('lime-toggle'); + + imeCtr.classList.remove('shift-toggle', 'symbol-toggle'); + sym = move = false; + + if (shift) { + shift = false; + caps = true; + f.classList.add('lime-toggle'); + } + else if (caps) + caps = false; + else + shift = true; + + if (shift || caps) + imeCtr.classList.add('shift-toggle'); + }, + + /** + * Shows symbol characters + */ + Symbol: function (f) { + var t = imeCtr.querySelector('.lime-toggle'); + + if (t) + t.classList.remove('lime-toggle'); + + sym = !sym; + shift = caps = move = false; + imeCtr.classList.remove('shift-toggle'); + imeCtr.classList.toggle('symbol-toggle'); + + if (sym) + f.classList.toggle('lime-toggle'); + }, + + /** + * Deletes the previous character or current selection + */ + Backspace: function () { + if (!isInput(document.activeElement)) + return; + + var sel = window.getSelection(); + + // If no characters are selected, select the previous + if (!String(sel).length) + sel.modify('extend', 'backward', 'character'); + + // Remove the selected range(s) + sel.deleteFromDocument(); + }, + + /** + * Switches to caret-move mode + */ + Caret: function (f) { + var t = imeCtr.querySelector('.lime-toggle'); + + if (t) + t.classList.remove('lime-toggle'); + + sym = shift = caps = false; + imeCtr.classList.remove('shift-toggle', 'symbol-toggle'); + + move = !move; + + if (move) + f.classList.add('lime-toggle'); + } + }, + + // Input modes, matching the HTML5 spec + inputMode = { + verbatim: { + keys: { + // Default keys, separated into rows + standard: [ + [ '@', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=' ], + [ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']' ], + [ 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'" ], + [ 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/' ], + ], + + // Symbol keys + symbol: [ + [ '@', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=' ], + [ '~', '#', '`', '£', '$', '%', '^', '&', '(', ')', '[', ']' ], + [ '•', '€', '¥', '¡', '¿', '*', '|', '{', '}', ':', '"' ], + [ 'غ', '=', '!', '\\', '«', '»', '§', '<', '>', '?' ], + ] + }, + + // Characters with diacritics appear when certain keys are held + diacritics: { + a: 'äáâàåæ', + c: 'ç©', + d: 'ð', + e: 'ëéêè', + i: 'ïíîì', + m: 'µ', + n: 'ñ', + o: 'öóôòõø', + u: 'üúûù', + y: 'ÿý', + } + } + }; + +// Initialize the container +imeCtr.className = 'lime-container'; +imeCtr.lang = 'en'; + +// Create the stylesheet +document.head.insertBefore(style, document.head.firstChild); + +for (var i=0; i < cssRules.length; i++) + style.sheet.insertRule(cssRules[i], i); + +/** + * Tests adherence to the HTML5 spec for some input types. + * Spec says that email|number|date|etc. must throw an error if code attempts + * to get or modify the selection, which presents a problem for virtual keyboards. + */ +function wontThrowOnSelection() { + var dummy = document.createElement('input'); + dummy.type = 'number'; + + try { + dummy.selectionStart = 0; + return true; + } + catch (e) { + return false; + } +} + +/** + * Shows the keyboard if the current active element is an input + */ +function showIME () { + if (!isInput(document.activeElement)) + return; + + document.body.appendChild(imeCtr); + visible = true; +} + +/** + * Removes the keyboard from the document + */ +function hideIME () { + document.body.removeChild(imeCtr); + visible = false; +} + +/** + * Sets up the keyboard + */ +function initKeys () { + var html = '', + spKeys = { + row2: { + before: '
  • ', + after: '
  • ' + }, + row3: { + before: '
  • ', + after: '
  • |⇄
  • ' + }, + }, + dia = inputMode.verbatim.diacritics, + keys = inputMode.verbatim.keys; + + // Create a for the row and
  • s for each key + html += keys.standard.map(function (keyRow, rIdx) { + var sp = spKeys['row'+rIdx]; + + return ( + '' + + (sp && sp.before || '') + + keyRow.map(function (k, kIdx) { + return '
  • '; + }).join('') + + (sp && sp.after || '') + + '
    ' + ); + }).join(''); + + // Add the bottom row, mostly special keys + html += '' + + '
  • ' + + '
  • ' + + '
  • ' + + '
  •  
  • ' + + '
  • ' + + '
  • ' + + '
    '; + + imeCtr.innerHTML = html; +} + +/** + * Moves the keyboard's pseudo-focus to the passed element + */ +function newFocus(el) { + if (!el.classList.contains('lime-key')) + return; + + if (focused) + focused.classList.remove('lime-focus'); + + focused = el; + focused.classList.add('lime-focus'); +} + +/** + * Moves the keyboard's pseudo-focus in the direction given + */ +function moveKeyFocus(dir) { + var next; + + // Focus the first key if none are focused + if (!focused) + next = imeCtr.querySelector('.lime-key'); + + // Left and right are easy + else if (dir === 'left' || dir === 'right') { + next = focused[(dir === 'left' ? 'previous' : 'next') + 'Sibling']; + + // Wrap around + if (!next && !diacriticsMenu) + next = focused.parentNode[(dir === 'left' ? 'last' : 'first') + 'Child']; + } + + // Up/down need to use elementFromPoint() + else if (!diacriticsMenu) { + var rect = focused.getBoundingClientRect(), + hght = focused.offsetHeight, + y = rect.top + (dir === 'down' ? hght : -hght), + x = lastLeft; + + if (!lastLeft) + x = lastLeft = rect.left + (focused.offsetWidth / 2); + + // First check is slightly to the left from the center of the last key + next = document.elementFromPoint(x - 10, y); + + // If not found, check slightly to the right instead + if (!next.classList.contains('lime-key')) + next = document.elementFromPoint(x + 10, y); + + // Still nothing? Undo! + if (!next.classList.contains('lime-key')) + next = null; + } + + if (next) { + newFocus(next); + + // Set lastLeft only when moving horizontally + if (dir === 'left' || dir === 'right') + lastLeft = focused.getBoundingClientRect().left + (focused.offsetWidth / 2); + } + + // Allow the user to move out of the diacritics menu + else if (diacriticsMenu) { + var rect = focused.getBoundingClientRect(); + hideDiacritics(); + newFocus(document.elementFromPoint(rect.left, rect.top)); + + lastLeft = rect.left + (focused.offsetWidth / 2); + return moveKeyFocus(dir); + } +} + +/** + * Performs the action of the key that has pseudo-focus + */ +function doKeyAction () { + var + txt, sp, + k = focused, + + // Intl 402 locale-sensitive uppercasing, probably not much support yet + tuc = 'toLocaleUpperCase' in String.prototype ? 'toLocaleUpperCase' : 'toUpperCase'; + + // Look through special keys first + if (sp = spKeys[k.id.slice(4)]) + return sp(k); + + else { + // If symbols are activated, use the data-symbol attribute + txt = k.dataset[sym ? 'symbol' : 'text'] || k.dataset.text; + + if (!txt) + return; + + // Uppercase if shift or caps + if (shift || caps) + txt = txt[tuc](); + + // Reset shift after one key press + if (shift) { + shift = false; + imeCtr.classList.remove('shift-toggle'); + } + + var a = document.activeElement; + + // If the type supports selection, this is easy + if (supportAnySel || regSupportSel.test(a.type)) { + var ss = a.selectionStart; + + // Set the new value to pre-caret + new + post-caret + a.value = a.value.slice(0, ss) + txt + a.value.slice(a.selectionEnd); + + // Put the caret where it should be + a.selectionStart = a.selectionEnd = ss + txt.length; + } + + // For types number, email, date, et al + else { + var len, pre, post, + + // Get current selection + s = window.getSelection(); + + // Delete any existing contents + s.deleteFromDocument(); + + // Keep moving selection backward until the length stops increasing + do { + len = String(s).length; + s.modify('extend', 'backward', 'line'); + } + while (String(s).length !== len); + + // Store the selection, then delete it + pre = String(s); + s.deleteFromDocument(); + + // Keep moving selection forward until the length stops increasing + do { + len = String(s).length; + s.modify('extend', 'forward', 'line'); + } + while (String(s).length !== len); + + // Store the selection, then delete it + post = String(s); + s.deleteFromDocument(); + + // Setting attribute works around a bug in Blink/WebKit + a.setAttribute('value', a.defaultValue); + + // Recreate the contents with the new text added + a.value = pre + txt + post; + + // Move the selection to after the new text + a.select(); + s = window.getSelection(); + s.collapseToEnd(); + + // Move the caret to the correct location + while (len-- > 0) + s.modify('move', 'backward', 'character'); + } + } +} + +/** + * Prevents an event's default action and bubble + */ +function swallowEvt (evt) { + evt.preventDefault(); + evt.stopPropagation(); +} + +/** + * Submits the current form if there is one and hides the IME + */ +function submit () { + if (document.activeElement.form) + document.activeElement.form.submit(); + + hideIME(); +} + +/** + * Shows diacritical variatins of the selected character + */ +function showDiacritics () { + var menu, fRect, cRect, rRect, w, + dia = inputMode.verbatim.diacritics[focused && focused.dataset.text]; + + if (!focused || !dia) + return; + + // Reuse if diacratics menu already exists + if (!(menu = imeCtr.querySelector('.lime-diacritics-row'))) { + menu = document.createElement('menu'); + menu.className = 'lime-key-row lime-diacritics-row'; + } + + // Dim the main keys and highlight the base key + imeCtr.classList.add('lime-container-dim'); + focused.classList.add('lime-toggle'); + + // Get some rectangles to work out positioning + cRect = imeCtr.getBoundingClientRect(); + fRect = focused.getBoundingClientRect(); + rRect = focused.parentNode.lastChild.getBoundingClientRect(); + + menu.innerHTML = ''; + + // Add a
  • element for each diacritical character + Array.prototype.forEach.call(dia, function (char) { + var li = menu.appendChild(document.createElement('li')); + + li.className = 'lime-key lime-diacritical'; + li.dataset.text = char; + }); + + // Add to container + imeCtr.appendChild(menu); + diacriticsMenu = menu; + + // Set the position of the keys + w = menu.offsetWidth; + menu.style.left = menu.style.right = ''; + + // Align to right-side of base key unless it will overlap row edge + if (fRect.right + w <= rRect.right) + menu.style.left = fRect.right - cRect.left + 'px'; + else + menu.style.right = cRect.right - fRect.left + 'px'; + + menu.style.top = fRect.top - cRect.top + 'px'; + + // Set width on a timer to allow for CSS transitions + menu.style.width = 0; + window.setTimeout(function () { + menu.style.width = w + 'px'; + }); +} + +/** + * Hides the diacritics menu + */ +function hideDiacritics () { + if (!diacriticsMenu) + return; + + // Remove the diacritics menu + diacriticsMenu.parentNode.removeChild(diacriticsMenu); + imeCtr.classList.remove('lime-container-dim'); + + // Refocus the non-diacritical version of the character + newFocus(imeCtr.querySelector('.lime-toggle[data-text]')); + focused.classList.remove('lime-toggle'); + + // Reset lastLeft, in case the user moved through the diacritics + lastLeft = focused.getBoundingClientRect().left; + diacriticsMenu = null; +} + +/** + * Selects next element in tabbing order + */ +function tabNext () { + var next, + + // Get element list as a real array so we can sort it + n = Array.prototype.slice.call(document.getElementsByTagName('*')); + + // Filter out untabables + n = n.filter(function (a) { return a.tabIndex > -1; }); + + n.sort(function (a, b) { + // Use Number.MAX_VALUE instead of 0 to sort to end + return (a.tabIndex||Number.MAX_VALUE) - (b.tabIndex||Number.MAX_VALUE); + }); + + // Find the element following the currently focused element + next = n[n.indexOf(document.activeElement) + 1]; + + // Wrap from last to first + if (!next) + next = n[0]; + + if (next) + next.focus(); +} + +/** + * Checks an element can receive text input and returns a boolean accordingly + */ +function isInput (el) { + var + // Regex to match inputs needing the IME + allowed = /^(?:text|email|number|password|tel|url|search)$/, + + // These don't allow selection, which may break the IME + toText = /^(?:email|number)$/; + + return (allowed.test(el.type) || el.isContentEditable) && !el.readOnly; +} + +/** + * Automatically shows the IME on focus if settings permit + */ +document.addEventListener('DOMFocusIn', function (evt) { + var tag = evt.target.tagName; + + if (!exports.config.noauto && (evt.target.isContentEditable || tag === 'INPUT' || tag === 'TEXTAREA')) + showIME(); +}); + +/** + * Automatically shows the IME on focus if settings permit + */ +document.addEventListener('DOMFocusOut', function (evt) { + if (visible && !exports.config.noauto) + hideIME(); +}); + +/** + * Captures and handles keydown events before they hit any elements + */ +document.addEventListener('keydown', function (evt) { + if (!visible) + return; + + var dia; + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'ArrowUp': + case 'ArrowDown': + // When the "move caret" button is toggled, arrow keys behave as normal + if (move) + return; + + swallowEvt(evt); + moveKeyFocus(evt.key.slice(5).toLowerCase()); + break; + + case 'Enter': + if (!holdTimer) + holdTimer = setTimeout(showDiacritics, 500); + + swallowEvt(evt); + break; + + case 'Escape': + case 'BrowserBack': + swallowEvt(evt); + + if (move) + spKeys.Caret(focused); + + else if (diacriticsMenu) + hideDiacritics(); + + else + hideIME(); + } +}, true); + +/** + * Fan service: flash the buttons as they are pressed on a hardware keyboard + */ +document.addEventListener('keypress', function (evt) { + var s = String.fromCharCode(evt.keyCode).toLowerCase(), + e = imeCtr.querySelector('li[data-text="'+s+'"]'); + + if (e) { + newFocus(e); + window.setTimeout(function () { + e.classList.remove('lime-focus'); + }, 100); + } +}, true); + +/** + * Text entry happens on keyup + */ +document.addEventListener('keyup', function (evt) { + if (!visible) + return; + + var dia; + switch (evt.key) { + case 'Escape': + case 'BrowserBack': + case 'ArrowUp': + case 'ArrowDown': + case 'ArrowLeft': + case 'ArrowRight': + swallowEvt(evt); + break; + + case 'Enter': + swallowEvt(evt); + clearTimeout(holdTimer); + holdTimer = null; + + if (diacriticsMenu && !focused.classList.contains('lime-diacritical')) + newFocus(diacriticsMenu.firstChild); + + else { + doKeyAction(); + + if (diacriticsMenu) + hideDiacritics(); + } + } +}, true); + +/** + * Handles mousedown on the keys, prevents input losing focus + */ +imeCtr.addEventListener('mousedown', function (evt) { + swallowEvt(evt); + + var tgt = evt.target, + act = document.activeElement; + + if (!evt.target.classList.contains('lime-key')) + return; + + holdTimer = setTimeout(showDiacritics, 500); +}, true); + +/** + * Mouseup is similar to on keyup for Enter key + */ +imeCtr.addEventListener('mouseup', function (evt) { + swallowEvt(evt); + + var tgt = evt.target, + act = document.activeElement; + + if (!evt.target.classList.contains('lime-key')) + return; + + clearTimeout(holdTimer); + holdTimer = null; + doKeyAction(); + + if (diacriticsMenu) + hideDiacritics(); +}, true); + +/** + * Move key focus for the mouse too + */ +imeCtr.addEventListener('mousemove', function (evt) { + newFocus(evt.target); +}); + +/** + * Remove focus on mouseout (mostly for the container) + */ +imeCtr.addEventListener('mouseout', function (evt) { + if (focused === evt.target) + focused.classList.remove('lime-focus'); +}); + +/** + * Event.key mini-polyfill + * Adds a getter to key events that maps to a few key spec strings + */ +(function () { + var map = { + 13: 'Enter', + 27: 'Escape', + + 37: 'ArrowLeft', + 38: 'ArrowUp', + 39: 'ArrowRight', + 40: 'ArrowDown', + }, + prop = { get: function () { + var code = this.which; + + return map[code] || 'Unidentified'; + }}; + + if (global.KeyboardEvent && !global.KeyboardEvent.prototype.hasOwnProperty('key')) + Object.defineProperty(global.KeyboardEvent.prototype, 'key', prop); + + if (global.KeyEvent && !global.KeyEvent.prototype.hasOwnProperty('key')) + Object.defineProperty(global.KeyEvent.prototype, 'key', prop); + +})(global); + +// Create the IME HTML +initKeys(); + +// Export a few functions +exports.show = showIME; +exports.hide = hideIME; + +return exports; + +}); diff --git a/keylime.min.js b/keylime.min.js new file mode 100644 index 0000000..5733330 --- /dev/null +++ b/keylime.min.js @@ -0,0 +1 @@ +(function(e,t){var i,o=t(e);if(typeof define==="function"&&define.amd)define(i=o);if(typeof exports==="object")module.exports=i=o;e.keyLime=o})(typeof global!=="undefined"?global:this,function(e){"use strict";var t,i,o,l,n,r,a,s,c,m=document.createElement("style"),d=document.createElement("div"),f=e.keyLime||{config:{}},u=false,g=[".lime-container { background-color: #333; position: absolute; bottom: 0; left: 0; right: 0; color: #fff; z-index:1000000; }",'.lime-container-dim::before { position: absolute; content: ""; top: 0; left: 0; right: 0; bottom: 0; background-color: #000; opacity: 0.5; }',".lime-key-row { list-style-type: none; clear: both; text-align: center; padding: 0; margin: 0; font-size: 28px; font-weight: bold; }",".lime-diacritics-row { position: absolute; z-index: 2; overflow: hidden; -webkit-transition: width 400ms; transition: width 400ms; white-space: nowrap; }",".lime-key { vertical-align: top; display: inline-block; border: 3px solid #333; background-color: #666; width: 66px; line-height: 50px; -webkit-transition: all 400ms linear; transition: all 400ms linear; }",".lime-key[data-text]::before { content: attr(data-text) }",".lime-container.symbol-toggle .lime-key[data-symbol]::before { content: attr(data-symbol) }",".lime-container.shift-toggle .lime-key[data-text]:not(.lime-http):not(.lime-dotcom):not(.lime-wwwdot)::before { text-transform: uppercase; }",".lime-special-key { background-color: #999; color: #333; }",".lime-return { background-color: #ccc; width: 138px; }",".lime-focus { background-color: #26f; color: #fff; }",".lime-toggle { background-color: #2f2; color: #fff; position: relative; }",".lime-spacebar { width:498px; }",".lime-http, .lime-dotcom, .lime-wwwdot { font-size: 20px; }"],y=/^(?:text|search|url|tel|password)$/i,p=b(),h={Tab:N,Return:B,Shift:function(e){var t=d.querySelector(".lime-toggle");if(t)t.classList.remove("lime-toggle");d.classList.remove("shift-toggle","symbol-toggle");n=r=false;if(o){o=false;l=true;e.classList.add("lime-toggle")}else if(l)l=false;else o=true;if(o||l)d.classList.add("shift-toggle")},Symbol:function(e){var t=d.querySelector(".lime-toggle");if(t)t.classList.remove("lime-toggle");n=!n;o=l=r=false;d.classList.remove("shift-toggle");d.classList.toggle("symbol-toggle");if(n)e.classList.toggle("lime-toggle")},Backspace:function(){if(!D(document.activeElement))return;var e=window.getSelection();if(!String(e).length)e.modify("extend","backward","character");e.deleteFromDocument()},Caret:function(e){var t=d.querySelector(".lime-toggle");if(t)t.classList.remove("lime-toggle");n=o=l=false;d.classList.remove("shift-toggle","symbol-toggle");r=!r;if(r)e.classList.add("lime-toggle")}},v={verbatim:{keys:{standard:[["@","1","2","3","4","5","6","7","8","9","0","-","="],["q","w","e","r","t","y","u","i","o","p","[","]"],["a","s","d","f","g","h","j","k","l",";","'"],["z","x","c","v","b","n","m",",",".","/"]],symbol:[["@","1","2","3","4","5","6","7","8","9","0","-","="],["~","#","`","£","$","%","^","&","(",")","[","]"],["•","€","¥","¡","¿","*","|","{","}",":","""],["غ","=","!","\\","«","»","§","<",">","?"]]},diacritics:{a:"äáâàåæ",c:"ç©",d:"ð",e:"ëéêè",i:"ïíîì",m:"µ",n:"ñ",o:"öóôòõø",u:"üúûù",y:"ÿý"}}};d.className="lime-container";d.lang="en";document.head.insertBefore(m,document.head.firstChild);for(var w=0;w
  • ',after:'
  • '},row3:{before:'
  • ',after:'
  • |⇄
  • '}},i=v.verbatim.diacritics,o=v.verbatim.keys;e+=o.standard.map(function(e,i){var l=t["row"+i];return''+(l&&l.before||"")+e.map(function(e,t){return'
  • '}).join("")+(l&&l.after||"")+"
    "}).join("");e+=''+'
  • '+'
  • '+'
  • '+'
  •  
  • '+'
  • '+'
  • '+"
    ";d.innerHTML=e}function x(e){if(!e.classList.contains("lime-key"))return;if(i)i.classList.remove("lime-focus");i=e;i.classList.add("lime-focus")}function C(e){var t;if(!i)t=d.querySelector(".lime-key");else if(e==="left"||e==="right"){t=i[(e==="left"?"previous":"next")+"Sibling"];if(!t&&!c)t=i.parentNode[(e==="left"?"last":"first")+"Child"]}else if(!c){var o=i.getBoundingClientRect(),l=i.offsetHeight,n=o.top+(e==="down"?l:-l),r=a;if(!a)r=a=o.left+i.offsetWidth/2;t=document.elementFromPoint(r-10,n);if(!t.classList.contains("lime-key"))t=document.elementFromPoint(r+10,n);if(!t.classList.contains("lime-key"))t=null}if(t){x(t);if(e==="left"||e==="right")a=i.getBoundingClientRect().left+i.offsetWidth/2}else if(c){var o=i.getBoundingClientRect();R();x(document.elementFromPoint(o.left,o.top));a=o.left+i.offsetWidth/2;return C(e)}}function S(){var e,t,r=i,a="toLocaleUpperCase"in String.prototype?"toLocaleUpperCase":"toUpperCase";if(t=h[r.id.slice(4)])return t(r);else{e=r.dataset[n?"symbol":"text"]||r.dataset.text;if(!e)return;if(o||l)e=e[a]();if(o){o=false;d.classList.remove("shift-toggle")}var s=document.activeElement;if(p||y.test(s.type)){var c=s.selectionStart;s.value=s.value.slice(0,c)+e+s.value.slice(s.selectionEnd);s.selectionStart=s.selectionEnd=c+e.length}else{var m,f,u,g=window.getSelection();g.deleteFromDocument();do{m=String(g).length;g.modify("extend","backward","line")}while(String(g).length!==m);f=String(g);g.deleteFromDocument();do{m=String(g).length;g.modify("extend","forward","line")}while(String(g).length!==m);u=String(g);g.deleteFromDocument();s.setAttribute("value",s.defaultValue);s.value=f+e+u;s.select();g=window.getSelection();g.collapseToEnd();while(m-->0)g.modify("move","backward","character")}}}function A(e){e.preventDefault();e.stopPropagation()}function B(){if(document.activeElement.form)document.activeElement.form.submit();L()}function T(){var e,t,o,l,n,r=v.verbatim.diacritics[i&&i.dataset.text];if(!i||!r)return;if(!(e=d.querySelector(".lime-diacritics-row"))){e=document.createElement("menu");e.className="lime-key-row lime-diacritics-row"}d.classList.add("lime-container-dim");i.classList.add("lime-toggle");o=d.getBoundingClientRect();t=i.getBoundingClientRect();l=i.parentNode.lastChild.getBoundingClientRect();e.innerHTML="";Array.prototype.forEach.call(r,function(t){var i=e.appendChild(document.createElement("li"));i.className="lime-key lime-diacritical";i.dataset.text=t});d.appendChild(e);c=e;n=e.offsetWidth;e.style.left=e.style.right="";if(t.right+n<=l.right)e.style.left=t.right-o.left+"px";else e.style.right=o.right-t.left+"px";e.style.top=t.top-o.top+"px";e.style.width=0;window.setTimeout(function(){e.style.width=n+"px"})}function R(){if(!c)return;c.parentNode.removeChild(c);d.classList.remove("lime-container-dim");x(d.querySelector(".lime-toggle[data-text]"));i.classList.remove("lime-toggle");a=i.getBoundingClientRect().left;c=null}function N(){var e,t=Array.prototype.slice.call(document.getElementsByTagName("*"));t=t.filter(function(e){return e.tabIndex>-1});t.sort(function(e,t){return(e.tabIndex||Number.MAX_VALUE)-(t.tabIndex||Number.MAX_VALUE)});e=t[t.indexOf(document.activeElement)+1];if(!e)e=t[0];if(e)e.focus()}function D(e){var t=/^(?:text|email|number|password|tel|url|search)$/,i=/^(?:email|number)$/;return(t.test(e.type)||e.isContentEditable)&&!e.readOnly}document.addEventListener("DOMFocusIn",function(e){var t=e.target.tagName;if(!f.config.noauto&&(e.target.isContentEditable||t==="INPUT"||t==="TEXTAREA"))k()});document.addEventListener("DOMFocusOut",function(e){if(u&&!f.config.noauto)L()});document.addEventListener("keydown",function(e){if(!u)return;var o;switch(e.key){case"ArrowLeft":case"ArrowRight":case"ArrowUp":case"ArrowDown":if(r)return;A(e);C(e.key.slice(5).toLowerCase());break;case"Enter":if(!t)t=setTimeout(T,500);A(e);break;case"Escape":case"BrowserBack":A(e);if(r)h.Caret(i);else if(c)R();else L()}},true);document.addEventListener("keypress",function(e){var t=String.fromCharCode(e.keyCode).toLowerCase(),i=d.querySelector('li[data-text="'+t+'"]');if(i){x(i);window.setTimeout(function(){i.classList.remove("lime-focus")},100)}},true);document.addEventListener("keyup",function(e){if(!u)return;var o;switch(e.key){case"Escape":case"BrowserBack":case"ArrowUp":case"ArrowDown":case"ArrowLeft":case"ArrowRight":A(e);break;case"Enter":A(e);clearTimeout(t);t=null;if(c&&!i.classList.contains("lime-diacritical"))x(c.firstChild);else{S();if(c)R()}}},true);d.addEventListener("mousedown",function(e){A(e);var i=e.target,o=document.activeElement;if(!e.target.classList.contains("lime-key"))return;t=setTimeout(T,500)},true);d.addEventListener("mouseup",function(e){A(e);var i=e.target,o=document.activeElement;if(!e.target.classList.contains("lime-key"))return;clearTimeout(t);t=null;S();if(c)R()},true);d.addEventListener("mousemove",function(e){x(e.target)});d.addEventListener("mouseout",function(e){if(i===e.target)i.classList.remove("lime-focus")});(function(){var t={13:"Enter",27:"Escape",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown"},i={get:function(){var e=this.which;return t[e]||"Unidentified"}};if(e.KeyboardEvent&&!e.KeyboardEvent.prototype.hasOwnProperty("key"))Object.defineProperty(e.KeyboardEvent.prototype,"key",i);if(e.KeyEvent&&!e.KeyEvent.prototype.hasOwnProperty("key"))Object.defineProperty(e.KeyEvent.prototype,"key",i)})(e);E();f.show=k;f.hide=L;return f}); \ No newline at end of file