/*!
* Copyright (C) 2017 Dremio Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint no-bitwise: 0 */
/**
* Base module for active-keys.
* Default export is singleton instance of KeyWatcher.
* @module index
* @example
* import keyWatcher from 'active-keys';
* keyWatcher.addEventListener('change', () => {
* console.log(Object.keys(keyWatcher.activeKeys));
* });
*/
import EventTargetShim from 'event-target-shim';
/**
* Tracks which keys are currently held down.
*/
export class KeyWatcher extends EventTargetShim {
/**
* Object of which keyboard keys are currently held down.
* Object keys are {@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values|KeyboardEvent#key}.
* Object values should be treated as truthy/falsy only.
* @fires {@link module:index.KeyWatcher#change|change} when updated.
*/
activeKeys = {};
_isListeningForEventsWithModifierKeys = false;
/**
* @method module:index.KeyWatcher#addEventListener
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
*/
/**
* @method module:index.KeyWatcher#removeEventListener
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
*/
constructor() {
super();
window.addEventListener('keydown', this);
window.addEventListener('keyup', this);
window.addEventListener('blur', this);
}
_destroy() {
window.removeEventListener('keydown', this);
window.removeEventListener('keyup', this);
window.removeEventListener('blur', this);
this._forEachEventWithModifierKeys('removeEventListener');
}
_forEachEventWithModifierKeys(method) {
const opts = PASSIVE_SUPPORTED ? {passive: true, capture: true} : true;
for (const eventType of EVENTS_WITH_MODIFIER_KEYS) {
window[method](eventType, this, opts);
}
}
/**
* @private
*/
handleEvent(evt) {
const typeHandler = '_handle' + evt.type[0].toUpperCase() + evt.type.slice(1);
let ret;
if (this[typeHandler]) {
ret = this[typeHandler](evt);
} else if (EVENTS_WITH_MODIFIER_KEYS.has(evt.type)) {
ret = this._handleEventWithModifierKey(evt);
} else {
console.warn(`No event handler for "${evt.type}" on KeyWatcher.`);
}
if (this._isListeningForEventsWithModifierKeys !== this._eventModifierKeyIsActive) {
if (this._isListeningForEventsWithModifierKeys) {
this._isListeningForEventsWithModifierKeys = false;
this._forEachEventWithModifierKeys('removeEventListener');
} else {
this._isListeningForEventsWithModifierKeys = true;
this._forEachEventWithModifierKeys('addEventListener');
}
}
return ret;
}
_handleEventWithModifierKey(evt) {
const changed = this._removeStuckModifiers(evt);
changed && this._dispatch();
}
_handleKeydown(evt) {
let {key, location} = evt;
let [newKey, changed] = this._handleModifiers(key);
key = newKey;
changed = this._removeStuckModifiers(evt) || changed; // always do _removeStuckModifiers()
if (key) {
const wasActive = this.activeKeys[key] = this.activeKeys[key] || 0;
const bitwise = 1 << location;
if (!(this.activeKeys[key] & bitwise)) {
this.activeKeys[key] |= bitwise;
if (!wasActive) changed = true;
}
}
changed && this._dispatch();
}
_handleKeyup(evt) {
let {key, location} = evt;
let [newKey, changed] = this._handleModifiers(key);
key = newKey;
changed = this._removeStuckModifiers(evt) || changed; // always do _removeStuckModifiers()
if (key) {
if (this.activeKeys[key]) {
const bitwiseInverse = ~(1 << location);
this.activeKeys[key] &= bitwiseInverse;
if (!this.activeKeys[key]) {
delete this.activeKeys[key];
changed = true;
}
}
}
changed && this._dispatch();
}
_handleBlur() {
// once the window/tab/frame loses focus we won't get keyup events
// so err on the side of a full reset.
// e.g. new tab, app switching, print dialog
this._removeAll();
}
_removeAll() {
// maintain the object reference
for (const activeKey of Object.keys(this.activeKeys)) {
delete this.activeKeys[activeKey];
}
this._dispatch();
}
_isNamedKey(key) {
return key.match(/^[A-Z][a-zA-Z0-9]+$/); // named keys match this pattern, while unnamed keys cannot (https://www.w3.org/TR/2017/CR-uievents-key-20170601/)
}
_isModifierKey(key) {
// these are the keys the spec specifies: https://www.w3.org/TR/2017/CR-uievents-key-20170601/#selecting-key-attribute-values
if (key === 'Shift' || key === 'CapsLock' || key === 'AltGraph') {
return true;
}
// These also have impact though.
if (key === 'Meta' || key === 'Alt' || key === 'Control') {
return true;
}
return false;
}
_removeNonModifierKeys() {
let removed = false;
for (const activeKey of Object.keys(this.activeKeys)) {
if (this._isModifierKey(activeKey)) continue;
delete this.activeKeys[activeKey];
removed = true;
}
return removed;
}
_removeStuckModifiers({altKey, ctrlKey, metaKey, shiftKey}) {
let changed = false;
if (this.activeKeys.Alt && !altKey) {
delete this.activeKeys.Alt;
changed = true;
}
if (this.activeKeys.Control && !ctrlKey) {
delete this.activeKeys.Control;
changed = true;
}
if (this.activeKeys.Meta && !metaKey) {
delete this.activeKeys.Meta;
changed = true;
}
if (this.activeKeys.Shift && !shiftKey) {
delete this.activeKeys.Shift;
changed = true;
}
return changed;
}
get _eventModifierKeyIsActive() {
return !!(this.activeKeys.Alt || this.activeKeys.Control || this.activeKeys.Meta || this.activeKeys.Shift);
}
_handleModifiers(key) {
let changed = false;
// Safety for browser/OS shortcuts
// While Chrome might be detected with missing keypress, FF cannot be.
// So lacking a better idea for now, being a bit aggressive...
// Also handles respected modifier safety.
if (this._isNamedKey(key)) {
changed = this._removeNonModifierKeys() || changed; // always do _removeNonModifierKeys()
}
// The Dead key can also get stuck, and it's not a real key, so just ignore it.
// e.g. on a US Mac keyboard;
// - down:e, down:Alt [down:Dead], up:alt, up:e -> Dead
return [key === 'Dead' ? null : key, changed];
}
_dispatch() {
/**
* Event fired when {@link module:index.KeyWatcher#activeKeys|activeKeys} changes.
* @event module:index.KeyWatcher#change
*/
const event = new Event('change', {
bubbles: false,
cancelable: false
});
this.dispatchEvent(event);
}
}
export default new KeyWatcher();
// not sure if this is complete. Likely need to add more over time:
// (KeyboardEvents skipped because keyup/keydown already handled and should suffice)
const EVENTS_WITH_MODIFIER_KEYS = new Set(`
touchstart
touchend
touchmove
touchcancel
click
dblclick
mousedown
mouseenter
mouseleave
mousemove
mouseout
mouseover
mouseup
contextmenu
dragstart
drag
dragenter
dragexit
dragleave
dragover
drop
dragend
wheel
`.split('\n').filter(Boolean));
const PASSIVE_SUPPORTED = (() => {
let passiveSupported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
passiveSupported = true;
}
});
window.addEventListener('passive-support-test', null, options);
} catch (error) {
// ignore
}
return passiveSupported;
})();