364 lines
11 KiB
JavaScript
364 lines
11 KiB
JavaScript
|
if( !exports ) var exports = {};
|
||
|
(function(exports, global){
|
||
|
"use strict";
|
||
|
|
||
|
function splice(str, index, count, add) {
|
||
|
return str.slice(0, index) + (add || "") + str.slice(index + count);
|
||
|
}
|
||
|
|
||
|
function checkSelectionSupport(type){
|
||
|
if (["text", "search", "URL", "tel", "password"].indexOf(type) == -1 ) return false;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function dispatchKeyboardOpenEvent(){
|
||
|
document.dispatchEvent(new CustomEvent('keyboard-open'));
|
||
|
}
|
||
|
|
||
|
function dispatchKeyboardCloseEvent(){
|
||
|
document.dispatchEvent(new CustomEvent('keyboard-close'));
|
||
|
}
|
||
|
|
||
|
function Element(Keyboard, el){
|
||
|
|
||
|
this.Keyboard = Keyboard;
|
||
|
|
||
|
el.layout = global.Keyboard.layout[el.type] ? el.type : '_default';
|
||
|
|
||
|
function dispatchEvent(event, keyInfo){
|
||
|
|
||
|
var event = new KeyboardEvent(event,{
|
||
|
key: keyInfo.symbol,
|
||
|
code: 'Key' + keyInfo.symbol,
|
||
|
keyCode : keyInfo.symbol.charCodeAt(0),
|
||
|
which : keyInfo.symbol.charCodeAt(0),
|
||
|
altKey : false,
|
||
|
ctrlKey : false,
|
||
|
shiftKey : false,
|
||
|
metaKey : false
|
||
|
});
|
||
|
event.virtual = true;
|
||
|
el.dispatchEvent(event);
|
||
|
}
|
||
|
|
||
|
this.onEvent = function(keyInfo){
|
||
|
dispatchEvent('keydown', keyInfo);
|
||
|
dispatchEvent('keypress', keyInfo);
|
||
|
dispatchEvent('keyup', keyInfo);
|
||
|
dispatchEvent('input', keyInfo);
|
||
|
dispatchEvent('change', keyInfo);
|
||
|
};
|
||
|
|
||
|
this.keydownfunc = function(e){
|
||
|
|
||
|
// If keydown is authentic, skip our internal update and let the default play out
|
||
|
if (!e.virtual) return;
|
||
|
e.preventDefault();
|
||
|
|
||
|
var selectionSupported = checkSelectionSupport(this.type);
|
||
|
|
||
|
if (e.key == "backspace") {
|
||
|
if ( !selectionSupported ) {
|
||
|
this.value = this.value.slice(0, -1);
|
||
|
} else {
|
||
|
var pos = el.selectionStart;
|
||
|
var offset = el.selectionEnd-el.selectionStart? 0:1;
|
||
|
this.value = this.value.substring(0, el.selectionStart-offset) + this.value.slice(el.selectionEnd);
|
||
|
this.setSelectionRange(pos-offset, pos-offset);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Toggle will change the layout to a different one that is pre-determined
|
||
|
// An empty toggle key will toggle the keyboard back to default
|
||
|
if (e.key.substring(0, 6) == "toggle") {
|
||
|
var newType = e.key.substring(6);
|
||
|
|
||
|
if(newType.length >= 0 && global.Keyboard.layout[newType]) {
|
||
|
return this.Keyboard.Keyboard.show(newType);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if( this.value.length >= this.maxLength && this.maxLength != -1 ){
|
||
|
return;
|
||
|
}
|
||
|
var update = e.key || String.fromCharCode(e.keyCode);
|
||
|
|
||
|
// Support an input case that will capitalize letters as needed
|
||
|
if(( e.target.dataset.case === 'sentence' && (this.value.length === 0 || this.value.slice(-1) === "." || this.value.slice(-2) === ". " ))
|
||
|
|| ( e.target.dataset.case === 'capitalize' && this.value.slice(-1) === " " ))
|
||
|
{
|
||
|
update = update.toUpperCase();
|
||
|
}
|
||
|
|
||
|
// TODO Mimic selection for input elements that don't support selection api as well
|
||
|
// IDEA: selection-polyfill? get caret's pixel location instead?
|
||
|
if ( !selectionSupported ) {
|
||
|
this.value += update;
|
||
|
} else {
|
||
|
var pos = el.selectionStart + update.length;
|
||
|
this.value = splice(
|
||
|
this.value,
|
||
|
el.selectionStart,
|
||
|
el.selectionEnd-el.selectionStart,
|
||
|
update
|
||
|
);
|
||
|
this.setSelectionRange(pos, pos); // reset the position after the splice
|
||
|
}
|
||
|
// TODO Calculate scroll amount based on caret position
|
||
|
this.scrollLeft = this.scrollWidth;
|
||
|
};
|
||
|
|
||
|
el.addEventListener('focus', this.focus.bind(this));
|
||
|
el.addEventListener('blur', this.blur.bind(this));
|
||
|
el.addEventListener('keydown', this.keydownfunc);
|
||
|
}
|
||
|
|
||
|
Element.prototype.focus = function(e){
|
||
|
this.Keyboard.show(e.target.layout);
|
||
|
this.Keyboard.on('key', this.onEvent);
|
||
|
};
|
||
|
Element.prototype.blur = function(e){
|
||
|
this.Keyboard.hide(e.target.layout);
|
||
|
this.Keyboard.off('key', this.onEvent);
|
||
|
};
|
||
|
|
||
|
function Keyboard(inputs, holder){
|
||
|
|
||
|
var self = this;
|
||
|
|
||
|
Array.prototype.slice.call(inputs, 0).forEach(function(input){
|
||
|
input.Keyboard = new Element(self, input);
|
||
|
});
|
||
|
|
||
|
this.active = false;
|
||
|
this.listeners = {key:[]};
|
||
|
this.keyboardEl = null;
|
||
|
this.layout = null;
|
||
|
this.keyboardEl = document.createElement("div");
|
||
|
this.keyboardEl.classList.add("keyboard-container");
|
||
|
this.keyboardEl.addEventListener("mousedown", handleKeydownEvents.bind(this));
|
||
|
this.keyboardEl.addEventListener("mouseup", handleKeyupEvents.bind(this));
|
||
|
// TODO - find a way to enable this touchstart event again.
|
||
|
// It prevents the :active state from being triggered on keys
|
||
|
this.keyboardEl.addEventListener("touchstart", handleKeydownEvents.bind(this));
|
||
|
this.keyboardEl.addEventListener("touchend", handleKeyupEvents.bind(this));
|
||
|
|
||
|
// Generate keyboard HTML, bind events, insert them to given element
|
||
|
this.show = function (layout) {
|
||
|
// Clear any timers relating to keyhold
|
||
|
this.clearKeyHoldTimers();
|
||
|
|
||
|
if (!global.Keyboard.layout[layout]) throw new Error("keyboard initiation: Missing layout: " + layout);
|
||
|
if( self.layout && layout === self.layout && this.active){
|
||
|
return;
|
||
|
}
|
||
|
this.active = true;
|
||
|
|
||
|
self.layout = layout;
|
||
|
this.keyboardEl.innerHTML = "";
|
||
|
this.keyboardEl.classList.remove('keyboard-container-hidden');
|
||
|
|
||
|
var closeButton = document.createElement("span");
|
||
|
closeButton.classList.add('keyboard-close-button');
|
||
|
closeButton.innerHTML = '✖';
|
||
|
this.keyboardEl.appendChild(closeButton);
|
||
|
closeButton.onclick = function() {
|
||
|
document.activeElement.blur()
|
||
|
}.bind(this);
|
||
|
closeButton.ontouchstart = function() {
|
||
|
document.activeElement.blur()
|
||
|
}.bind(this);
|
||
|
|
||
|
function foreachLayout(row, rowIndex, layout) {
|
||
|
|
||
|
var rowEl = document.createElement("div");
|
||
|
rowEl.classList.add("keyboard-row");
|
||
|
rowEl.classList.add("keyboard-row--" + rowIndex);
|
||
|
|
||
|
function foreachRow(key, keyIndex, row) {
|
||
|
var keyEl = document.createElement("div");
|
||
|
keyEl.classList.add("keyboard-key");
|
||
|
keyEl.classList.add("keyboard-key--" + keyIndex);
|
||
|
// Parse the layout configuration
|
||
|
for (var dataName in key) {
|
||
|
switch (dataName) {
|
||
|
case "symbol":
|
||
|
if (!key.label) keyEl.innerHTML = key[dataName];
|
||
|
keyEl.dataset.symbol = key[dataName];
|
||
|
break;
|
||
|
case "label":
|
||
|
keyEl.innerHTML = key[dataName];
|
||
|
break;
|
||
|
default:
|
||
|
keyEl.dataset[dataName] = key[dataName];
|
||
|
}
|
||
|
}
|
||
|
rowEl.appendChild(keyEl);
|
||
|
}
|
||
|
row.forEach(foreachRow);
|
||
|
self.keyboardEl.appendChild(rowEl);
|
||
|
}
|
||
|
|
||
|
global.Keyboard.layout[self.layout].forEach(foreachLayout);
|
||
|
// Append keys to el
|
||
|
|
||
|
// Send an event signifying open of keyboard
|
||
|
dispatchKeyboardOpenEvent()
|
||
|
holder.appendChild(self.keyboardEl);
|
||
|
};
|
||
|
|
||
|
this.hide = function(){
|
||
|
self.active = false;
|
||
|
// Clear any timers relating to keyhold to cleanup
|
||
|
this.clearKeyHoldTimers();
|
||
|
// Send an event signifying close of keyboard
|
||
|
dispatchKeyboardCloseEvent()
|
||
|
setTimeout(function(){
|
||
|
if( self.active ) return;
|
||
|
self.keyboardEl.classList.add("keyboard-container-hidden");
|
||
|
self.keyboardEl.innerHTML = "";
|
||
|
},25);
|
||
|
}
|
||
|
|
||
|
this.on = function(evt, action){
|
||
|
if( !this.listeners[evt] ){
|
||
|
this.listeners[evt] = [];
|
||
|
}
|
||
|
this.listeners[evt].push(action);
|
||
|
};
|
||
|
this.off = function(evt, action){
|
||
|
if( !this.listeners[evt] ) return;
|
||
|
this.listeners[evt] = this.listeners[evt].filter(function(listener){
|
||
|
return action.toString() !== listener.toString();
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add input(s) after the fact
|
||
|
* @param inputs
|
||
|
*/
|
||
|
this.add = function(inputs){
|
||
|
|
||
|
if(!Array.isArray(inputs))
|
||
|
inputs = [inputs];
|
||
|
|
||
|
Array.prototype.slice.call(inputs, 0).forEach(function(input){
|
||
|
input.Keyboard = new Element(self, input);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Clear timer events bound to the keyboard
|
||
|
*/
|
||
|
this.clearKeyHoldTimers = function () {
|
||
|
// Clear timeout to make sure multiple keypress does not start
|
||
|
if(this.keyHoldTimeout) {
|
||
|
window.clearTimeout(this.keyHoldTimeout);
|
||
|
}
|
||
|
// Clear interval to make sure multiple keypress does not continue
|
||
|
if(this.keyHoldInterval) {
|
||
|
window.clearInterval(this.keyHoldInterval);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
|
||
|
// We would like to pipe all keyboard events through one handler
|
||
|
var handleKeydownEvents = function (e) {
|
||
|
var self = this;
|
||
|
e.preventDefault();
|
||
|
// Check to make sure it's a key that's pressed
|
||
|
if (!e.target.classList.contains("keyboard-key")) return;
|
||
|
|
||
|
// the :active pseudo class is not working with touch events
|
||
|
// use active class to get around this issue
|
||
|
e.target.classList.add('active');
|
||
|
|
||
|
var keyInfo = e.target.dataset;
|
||
|
|
||
|
self.listeners['key'].forEach(function(action){
|
||
|
action(keyInfo);
|
||
|
});
|
||
|
|
||
|
// Allow for a key to be input multiple times by holding it down
|
||
|
// Ensure that any current timers are cleared before continuing
|
||
|
self.clearKeyHoldTimers();
|
||
|
// Timeout will provide a delay to prevent accidental holding
|
||
|
// After that, the interval will provide repeated input
|
||
|
self.keyHoldTimeout = window.setTimeout(function() {
|
||
|
self.keyHoldInterval = window.setInterval(function() {
|
||
|
self.listeners['key'].forEach(function(action){
|
||
|
action(keyInfo);
|
||
|
});
|
||
|
}, 100)
|
||
|
}, 400)
|
||
|
};
|
||
|
|
||
|
var handleKeyupEvents = function (e) {
|
||
|
e.target.classList.remove('active');
|
||
|
|
||
|
// Clear timeout to make sure multiple keypress does not start
|
||
|
this.clearKeyHoldTimers();
|
||
|
}
|
||
|
|
||
|
global.Keyboard = Keyboard;
|
||
|
global.Keyboard.layout = {
|
||
|
_default: [
|
||
|
[
|
||
|
{"symbol": "Q"},
|
||
|
{"symbol": "W"},
|
||
|
{"symbol": "E"},
|
||
|
{"symbol": "R"},
|
||
|
{"symbol": "T"},
|
||
|
{"symbol": "Y"},
|
||
|
{"symbol": "U"},
|
||
|
{"symbol": "I"},
|
||
|
{"symbol": "O"},
|
||
|
{"symbol": "P"},
|
||
|
{"label": ".com", "symbol": ".COM"},
|
||
|
{"symbol": "7"},
|
||
|
{"symbol": "8"},
|
||
|
{"symbol": "9"},
|
||
|
{"label": "\u21E6", "symbol":"backspace"}
|
||
|
],
|
||
|
[
|
||
|
// {"label": "tab", "func": "tab"},
|
||
|
{"symbol": "A"},
|
||
|
{"symbol": "S"},
|
||
|
{"symbol": "D"},
|
||
|
{"symbol": "F"},
|
||
|
{"symbol": "G"},
|
||
|
{"symbol": "H"},
|
||
|
{"symbol": "J"},
|
||
|
{"symbol": "K"},
|
||
|
{"symbol": "L"},
|
||
|
{"symbol": "@"},
|
||
|
{"label": ".net", "symbol": ".NET"},
|
||
|
{"symbol": "4"},
|
||
|
{"symbol": "5"},
|
||
|
{"symbol": "6"}
|
||
|
],
|
||
|
[
|
||
|
{"symbol": "Z"},
|
||
|
{"symbol": "X"},
|
||
|
{"symbol": "C"},
|
||
|
{"symbol": "V"},
|
||
|
{"symbol": "B"},
|
||
|
{"symbol": "N"},
|
||
|
{"symbol": "M"},
|
||
|
{"symbol": "."},
|
||
|
{"symbol": "_"},
|
||
|
{"symbol": "-"},
|
||
|
{"symbol": " ", "label": "space"},
|
||
|
{"symbol": "0"},
|
||
|
{"symbol": "1"},
|
||
|
{"symbol": "2"},
|
||
|
{"symbol": "3"}
|
||
|
]
|
||
|
]
|
||
|
};
|
||
|
})(exports || {}, window);
|