UNPKG

@teachingtextbooks/keyboard

Version:

Customizable TypeScript soft keyboard

269 lines (268 loc) 9.33 kB
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; } }