@teachingtextbooks/keyboard
Version:
Customizable TypeScript soft keyboard
269 lines (268 loc) • 9.33 kB
JavaScript
import { KeyboardKeys } from "./const";
export default class TTFocusManager {
constructor(doc, keysConfig) {
this.LOG_ENABLED = false;
this.inputs = [];
this.fauxSet = new Map;
this.regSet = new Map;
this.tabIndex = 0;
this.keyboard = null;
this.doc = doc;
this.keysConfig = keysConfig;
this.regState = new RegularInputState;
this.docDownBind = this.onDocKeyDown.bind(this);
this.docUpBind = this.onDocKeyUp.bind(this);
this.doc.addEventListener('keydown', this.docDownBind);
this.doc.addEventListener('keyup', this.docUpBind);
}
set logging(bool) {
console.log(this, 'logging:', bool);
this.LOG_ENABLED = bool;
}
focus() {
this.log('focus', this.tabIndex);
const faux = this.getCurrentFaux();
const reg = this.getCurrentRegular();
if (faux) {
!faux.isFocused() && faux.focusIn(true);
}
else if (reg) {
reg.focus();
}
}
keyDown(key, code, shift) {
var _a;
this.log('key down:', this.tabIndex, ';', key, code, shift);
const faux = this.getCurrentFaux();
const reg = this.getCurrentRegular();
if (faux) {
!faux.isFocused() && faux.focusIn(true);
faux.input(key, code, shift);
}
else if (reg) {
const ss = (_a = reg.selectionStart) !== null && _a !== void 0 ? _a : 0;
let newVal = reg.value;
if (code == KeyboardKeys.BACKSPACE || code == KeyboardKeys.BACKSPACE_WIDE) {
if (ss > 0) {
newVal = reg.value.substring(0, ss - 1) + reg.value.substring(ss);
reg.value = newVal;
reg.selectionStart = reg.selectionEnd = this.regState.caret = ss - 1;
}
}
else if (code == KeyboardKeys.DELETE) {
if (ss < reg.value.length) {
newVal = reg.value.substring(0, ss) + reg.value.substring(ss + 1);
reg.value = newVal;
reg.selectionStart = reg.selectionEnd = this.regState.caret = ss;
}
}
else {
this.regState.middle = ss < reg.value.length;
const val = reg.value;
reg.value = val.substring(0, ss) + (shift ? key.toUpperCase() : key) + val.substring(ss);
reg.selectionStart = reg.selectionEnd = ss + key.length;
}
this.regState.input = reg;
/* Safari and Opera trick to update input scroll position */
reg.blur();
reg.focus();
reg.offsetWidth;
/* doesn't works in Safari */
reg.onscroll = this.onRegScroll.bind(this);
}
}
keyUp(key, code, shift) {
this.log('key up:', this.tabIndex, ';', key, code, shift);
let faux = this.getCurrentFaux();
let reg = this.getCurrentRegular();
if (code === KeyboardKeys.TAB) {
faux && faux.focusOut();
reg && reg.blur();
this.nextTab(shift);
faux = this.getCurrentFaux();
reg = this.getCurrentRegular();
faux ?
faux.focusIn() :
reg ?
reg.focus() :
null;
}
else if (faux) {
!faux.isFocused() && faux.focusIn(true);
}
else if (reg) {
reg.focus();
reg.onscroll = null;
this.regState.reset();
}
}
setKeyboard(val) {
this.log('set keyboard:', val);
this.keyboard = val;
}
setInputs(inputs) {
this.log('set inputs:', inputs);
this.inputs = inputs.concat();
this.fauxSet.clear();
this.regSet.clear();
this.tabIndex = 0;
this.inputs.map((input) => {
const faux = ('getID' in input) ? input : null;
const reg = !faux ? input : null;
if (faux) {
faux.setFocusCallbacks(this.onFauxFocusIn.bind(this), this.onFauxFocusOut.bind(this));
faux.setInputCallbacks(this.onFauxKeyDown.bind(this), this.onFauxKeyUp.bind(this));
this.fauxSet.set(faux, true);
}
else if (reg) {
this.regSet.set(reg, true);
reg.onfocus = this.onRegFocusIn.bind(this);
reg.onblur = this.onRegFocusOut.bind(this);
}
});
const faux = this.getCurrentFaux();
const reg = this.getCurrentRegular();
faux ?
faux.focusIn() :
reg ?
reg.focus() :
null;
}
nextTab(prev = false) {
const fromIndex = this.tabIndex;
this.log('next tab:', fromIndex);
while (true) {
if (!prev) {
this.tabIndex++;
if (this.tabIndex > (this.inputs.length - 1))
this.tabIndex = 0;
}
else {
this.tabIndex--;
if (this.tabIndex < 0)
this.tabIndex = this.inputs.length - 1;
}
const faux = this.getCurrentFaux();
if (!faux)
break;
else if (faux && faux.isEnabled())
break;
else if (this.tabIndex === fromIndex)
break;
}
}
onRegFocusIn(ev) {
const faux = this.getCurrentFaux();
const reg = ev.target;
faux && faux.focusOut();
this.tabIndex = this.inputs.indexOf(reg);
this.tabIndex = Math.max(this.tabIndex, 0);
}
onRegFocusOut(_ev) {
if (this.regState.input) {
const agent = navigator.userAgent.toLowerCase();
const isSafari = agent.indexOf('safari/') > -1;
const isChrome = agent.indexOf('chrome/') > -1;
const isOpera = agent.indexOf('opr/') > -1 || agent.indexOf('opera') > -1;
/* Safari trick to update input scroll position */
isSafari && !isOpera && !isChrome && this.regState.input.focus();
}
}
onRegScroll(_ev) {
var _a;
(_a = this.regState.input) === null || _a === void 0 ? void 0 : _a.focus();
}
onDocKeyDown(ev) {
var _a;
const keyConf = this.keysConfig.get(ev.code);
if (keyConf === null || keyConf === void 0 ? void 0 : keyConf.isTab) {
ev.preventDefault();
}
else {
const faux = this.getCurrentFaux();
const reg = this.getCurrentRegular();
if (reg) {
reg.focus();
}
else if (faux) {
!faux.isFocused() && faux.focusIn();
this.keyDown(ev.key, ev.code, ev.shiftKey);
}
}
(_a = this.keyboard) === null || _a === void 0 ? void 0 : _a.input(ev.code, true);
}
onDocKeyUp(ev) {
var _a;
this.keyUp(ev.key, ev.code, ev.shiftKey);
(_a = this.keyboard) === null || _a === void 0 ? void 0 : _a.input(ev.code, false);
}
onFauxFocusIn(input) {
const faux = this.getCurrentFaux();
const reg = this.getCurrentRegular();
reg && reg.blur();
this.log('focus in:', faux ? faux : reg, '->', input);
this.tabIndex = this.inputs.indexOf(input);
}
onFauxFocusOut(input) {
const faux = this.getCurrentFaux();
const reg = this.getCurrentRegular();
this.log('focus out:', faux ? faux : reg, '/', input);
}
onFauxKeyDown(_key, code, _shift) {
var _a;
(_a = this.keyboard) === null || _a === void 0 ? void 0 : _a.input(code, true);
}
onFauxKeyUp(_key, code, _shift) {
var _a;
(_a = this.keyboard) === null || _a === void 0 ? void 0 : _a.input(code, false);
}
dispose() {
this.log('dispose');
const fauxInputs = Array.from(this.fauxSet.keys());
fauxInputs.map((input) => {
input.resetFocusCallbacks();
input.resetInputCallbacks();
});
const regInputs = Array.from(this.regSet.keys());
regInputs.map((input) => {
input.onfocus = null;
input.onblur = null;
input.onscroll = null;
});
this.fauxSet.clear();
this.regSet.clear();
this.inputs.length = 0;
this.keyboard = null;
this.doc.removeEventListener('keydown', this.docDownBind);
this.doc.removeEventListener('keyup', this.docUpBind);
}
getCurrentFaux() {
const input = this.inputs[this.tabIndex];
return this.fauxSet.has(input) ?
input :
null;
}
getCurrentRegular() {
const input = this.inputs[this.tabIndex];
return this.regSet.has(input) ?
input :
null;
}
log(...args) {
if (!this.LOG_ENABLED)
return;
console.log.apply(this, ['TTFocusManager >', ...args]);
}
}
class RegularInputState {
constructor() {
this.input = null;
this.caret = 0;
this.middle = false;
}
reset() {
this.input = null;
this.caret = 0;
this.middle = false;
}
}