From 7fc3935f05a9427cdb5ed142f0f36ab1f6df5d23 Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 28 Apr 2019 23:30:56 -0500 Subject: [PATCH] feat: decouple input normalization from action registry --- TODO.md | 3 +- packages/input/action-registry.js | 79 +++++++++++++++++++ packages/input/index.js | 127 +----------------------------- packages/input/normalizer.js | 88 +++++++++++++++++++++ 4 files changed, 171 insertions(+), 126 deletions(-) create mode 100644 packages/input/action-registry.js create mode 100644 packages/input/normalizer.js diff --git a/TODO.md b/TODO.md index b212e56..8e6cbae 100644 --- a/TODO.md +++ b/TODO.md @@ -17,4 +17,5 @@ - ✔ EventEmitter::emit is too hot - ❌ entityList.fromJSON() - ❌ Socket WebWorker can't connect in Firefox -- ❌ Transients +- ✔ Entity packets +- ✔ Decouple input normalization from action registry diff --git a/packages/input/action-registry.js b/packages/input/action-registry.js new file mode 100644 index 0000000..2f6b84c --- /dev/null +++ b/packages/input/action-registry.js @@ -0,0 +1,79 @@ +import * as I from 'immutable'; + +import {compose} from '@avocado/core'; +import {EventEmitter} from '@avocado/mixins'; + +const decorate = compose( + EventEmitter, +); + +export class ActionRegistry extends decorate(class {}) { + + constructor() { + super(); + this.mapActionToKey = new Map(); + this.mapKeyToAction = {}; + this.normalizer = undefined; + this._state = I.Map(); + } + + actionForKey(key) { + return this.mapKeyToAction[key]; + } + + listen(normalizer) { + // Only listen once. + if (this.normalizer) { + return; + } + this.normalizer = normalizer; + this.normalizer.on('keyUp', this.onKeyUp, this); + this.normalizer.on('keyDown', this.onKeyDown, this); + } + + keyForAction(action) { + return this.mapActionToKey.get(action); + } + + mapKeysToActions(map) { + for (const key in map) { + const action = map[key]; + this.mapKeyToAction[key] = action; + this.mapActionToKey.set(action, key); + } + } + + onKeyDown(key) { + if (this.mapKeyToAction[key]) { + const action = this.mapKeyToAction[key]; + this._state = this._state.set(action, true); + } + } + + onKeyUp(key) { + if (this.mapKeyToAction[key]) { + const action = this.mapKeyToAction[key]; + this._state = this._state.delete(action); + } + } + + get state() { + return this._state; + } + + set state(state) { + this._state = state; + } + + stopListening(normalizer) { + if (!this.normalizer) { + return; + } + this.normalizer.off('keyUp', this.onKeyUp); + this.normalizer.off('keyDown', this.onKeyDown); + this.normalizer = undefined; + } + +} + +export {InputPacket} from './packet/input.packet'; diff --git a/packages/input/index.js b/packages/input/index.js index 6733f4d..5212b2d 100644 --- a/packages/input/index.js +++ b/packages/input/index.js @@ -1,126 +1,3 @@ -import * as I from 'immutable'; - -import {compose} from '@avocado/core'; -import {EventEmitter} from '@avocado/mixins'; - -const decorate = compose( - EventEmitter, -); - -export class ActionRegistry extends decorate(class {}) { - - static normalizeKey(key) { - return key.toLowerCase(); - } - - constructor() { - super(); - // Track events. - this.target = undefined; - this.mapActionToKey = new Map(); - this.mapKeyToAction = {}; - // Track actions. - this._state = I.Map(); - // Handle lame OS input event behavior. See: https://mzl.la/2Ob0WQE - this.keysDown = {}; - this.keyUpDelays = {}; - // Bind event handlers. - this.onBlur = this.onBlur.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - } - - actionForKey(key) { - return this.mapKeyToAction[key]; - } - - listen(target = window.document) { - // Only listen once. - if (this.target) { - return; - } - this.target = target; - this.target.addEventListener('blur', this.onBlur); - this.target.addEventListener('keydown', this.onKeyDown); - this.target.addEventListener('keyup', this.onKeyUp); - } - - keyForAction(action) { - return this.mapActionToKey.get(action); - } - - mapKeysToActions(map) { - for (const key in map) { - const action = map[key]; - this.mapKeyToAction[key] = action; - this.mapActionToKey.set(action, key); - } - } - - onBlur(event) { - event = event || window.event; - this.setAllKeysUp(); - } - - onKeyDown(event) { - event = event || window.event; - const key = this.constructor.normalizeKey(event.key); - if (this.keysDown[key]) { - if (this.keyUpDelays[key]) { - clearTimeout(this.keyUpDelays[key]); - delete this.keyUpDelays[key]; - } - return; - } - this.keysDown[key] = true; - if (this.mapKeyToAction[key]) { - const action = this.mapKeyToAction[key]; - this._state = this._state.set(action, true); - } - } - - onKeyUp(event) { - event = event || window.event; - const key = this.constructor.normalizeKey(event.key); - this.keyUpDelays[key] = setTimeout(() => { - delete this.keyUpDelays[key]; - delete this.keysDown[key]; - if (this.mapKeyToAction[key]) { - const action = this.mapKeyToAction[key]; - this._state = this._state.delete(action); - } - }, 20); - } - - setAllKeysUp() { - this.keysDown = {}; - for (const key in this.keyUpDelays) { - const handle = this.keyUpDelays[key]; - clearTimeout(handle); - } - this.keyUpDelays = {}; - this._state = I.Map(); - } - - get state() { - return this._state; - } - - set state(state) { - this._state = state; - } - - stopListening() { - this.setAllKeysUp(); - if (!this.target) { - return; - } - this.target.removeEventListener('blur', this.onBlur); - this.target.removeEventListener('keydown', this.onKeyDown); - this.target.removeEventListener('keyup', this.onKeyUp); - this.target = undefined; - } - -} - +export {ActionRegistry} from './action-registry'; +export {InputNormalizer} from './normalizer'; export {InputPacket} from './packet/input.packet'; diff --git a/packages/input/normalizer.js b/packages/input/normalizer.js new file mode 100644 index 0000000..aea9682 --- /dev/null +++ b/packages/input/normalizer.js @@ -0,0 +1,88 @@ +import {compose} from '@avocado/core'; +import {EventEmitter} from '@avocado/mixins'; + +const decorate = compose( + EventEmitter, +); + +export class InputNormalizer extends decorate(class{}) { + + constructor() { + super(); + // Track events. + this.target = undefined; + this.targetForKeyUp = undefined; + // Handle lame OS input event behavior. See: https://mzl.la/2Ob0WQE + this.keysDown = {}; + this.keyUpDelays = {}; + // Bind event handlers. + this.onBlur = this.onBlur.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onKeyUp = this.onKeyUp.bind(this); + } + + actionForKey(key) { + return this.mapKeyToAction[key]; + } + + listen(target = window.document, targetForKeyUp = window.document) { + // Only listen once. + if (this.target) { + return; + } + this.target = target; + this.targetForKeyUp = targetForKeyUp; + this.target.addEventListener('blur', this.onBlur); + this.target.addEventListener('keydown', this.onKeyDown); + this.targetForKeyUp.addEventListener('keyup', this.onKeyUp); + } + + onBlur(event) { + this.setAllKeysUp(); + } + + onKeyDown(event) { + const {key} = event; + if (this.keysDown[key]) { + if (this.keyUpDelays[key]) { + clearTimeout(this.keyUpDelays[key]); + delete this.keyUpDelays[key]; + } + return; + } + this.keysDown[key] = true; + this.emit('keyDown', key); + } + + onKeyUp(event) { + const {key} = event; + this.keyUpDelays[key] = setTimeout(() => { + delete this.keyUpDelays[key]; + delete this.keysDown[key]; + this.emit('keyUp', key); + }, 20); + } + + setAllKeysUp() { + this.keysDown = {}; + for (const key in this.keyUpDelays) { + const handle = this.keyUpDelays[key]; + clearTimeout(handle); + this.emit('keyUp', key); + } + this.keyUpDelays = {}; + } + + stopListening() { + this.setAllKeysUp(); + if (!this.target) { + return; + } + this.target.removeEventListener('blur', this.onBlur); + this.target.removeEventListener('keydown', this.onKeyDown); + this.targetForKeyUp.removeEventListener('keyup', this.onKeyUp); + this.target = undefined; + this.targetForKeyUp = undefined; + } + +}