@platform/react
Version:
React refs and helpers.
172 lines (171 loc) • 6.63 kB
JavaScript
import { Subject } from 'rxjs';
import { filter, map, share, take, takeUntil } from 'rxjs/operators';
import { events } from '../events';
import { equals, uniq } from 'ramda';
const R = { equals, uniq };
const MODIFIERS = {
META: 'metaKey',
CTRL: 'ctrlKey',
ALT: 'altKey',
SHIFT: 'shiftKey',
};
const isModifierPressed = (e) => {
return Object.keys(MODIFIERS)
.map((key) => MODIFIERS[key])
.some((prop) => e[prop] === true);
};
export class Keyboard {
constructor(options) {
this._dispose$ = new Subject();
this.dispose$ = this._dispose$.pipe(share());
this.bindings = [];
const bindingPress$ = new Subject();
this.bindingPress$ = bindingPress$.pipe(takeUntil(this._dispose$), share());
let keyPress$ = options.keyPress$ || events.keyPress$;
this.keyPress$ = keyPress$ = keyPress$.pipe(takeUntil(this._dispose$));
this.bindings = options.bindings || [];
keyPress$.subscribe((e) => (this.latest = e));
this.monitorBindings((e) => bindingPress$.next(e));
if (options.dispose$) {
options.dispose$.pipe(take(1)).subscribe(() => this.dispose());
}
}
static create(options) {
return new Keyboard(options);
}
static isModifier(key) {
return Object.keys(MODIFIERS).some((item) => item === key);
}
static parse(pattern, defaultValue) {
const EMPTY = { keys: [], modifiers: [] };
if (pattern === false) {
return EMPTY;
}
pattern = typeof pattern === 'string' ? pattern.trim() : pattern;
if ((pattern === true || !pattern) && typeof defaultValue === 'string') {
pattern = defaultValue;
}
if (!pattern || typeof pattern !== 'string') {
return EMPTY;
}
const parts = (pattern || '')
.split('+')
.map((key) => key.trim())
.filter((key) => Boolean(key))
.map((key) => Keyboard.formatKey(key));
const modifiers = Array.from(new Set(parts.filter(Keyboard.isModifier)));
let keys = parts.filter((key) => !modifiers.some((item) => item === key));
keys = Array.from(new Set(keys));
return { keys, modifiers };
}
static matchEvent(pattern, event) {
const key = Keyboard.formatKey(event.key);
pattern = typeof pattern === 'string' ? Keyboard.parse(pattern) : pattern;
if (!pattern.keys.includes(key)) {
return false;
}
let eventModifiers = [];
eventModifiers = event.metaKey ? [...eventModifiers, 'META'] : eventModifiers;
eventModifiers = event.ctrlKey ? [...eventModifiers, 'CTRL'] : eventModifiers;
eventModifiers = event.altKey ? [...eventModifiers, 'ALT'] : eventModifiers;
eventModifiers = event.shiftKey ? [...eventModifiers, 'SHIFT'] : eventModifiers;
if (eventModifiers.length !== pattern.modifiers.length) {
return false;
}
for (const modifier of eventModifiers) {
if (!pattern.modifiers.includes(modifier)) {
return false;
}
}
return true;
}
static formatKey(key) {
key = key || '';
const MODIFIERS = ['CMD', 'COMMAND', 'META', 'CONTROL', 'CTRL', 'ALT', 'SHIFT'];
key = MODIFIERS.includes(key.toUpperCase()) ? key.toUpperCase() : key;
key = key === 'CMD' ? 'META' : key;
key = key === 'COMMAND' ? 'META' : key;
key = key === 'CONTROL' ? 'CTRL' : key;
key = key.length === 1 ? key.toUpperCase() : key;
return key;
}
get isDisposed() {
return this._dispose$.isStopped;
}
dispose() {
this._dispose$.next();
this._dispose$.complete();
}
clone(options = {}) {
const dispose$ = options.dispose$;
const keyPress$ = options.keyPress$ || this.keyPress$;
const bindings = options.bindings || this.bindings;
return Keyboard.create({ keyPress$, dispose$, bindings });
}
filter(fn) {
const keyPress$ = this.keyPress$.pipe(filter(fn));
return this.clone({ keyPress$ });
}
takeUntil(dispose$) {
return this.clone({ dispose$ });
}
monitorBindings(fire) {
const keyPress$ = this.keyPress$;
let pressedKeys = [];
keyPress$
.subscribe((e) => {
const hasModifier = isModifierPressed(e);
if (e.isModifier && !e.isPressed && !hasModifier) {
pressedKeys = [];
}
if (!e.isModifier) {
let key = e.code;
key = key.startsWith('Key') ? e.code.replace(/^Key/, '') : key;
key = key.startsWith('Numpad') ? e.code.replace(/^Numpad/, '') : key;
key = key.startsWith('Digit') ? e.code.replace(/^Digit/, '') : key;
key = key.length === 1 ? key.toUpperCase() : key;
pressedKeys = e.isPressed ? R.uniq([...pressedKeys, key]) : [];
}
});
keyPress$
.pipe(filter((e) => e.isPressed), map((e) => ({ event: e, binding: this.matchBinding(e, pressedKeys) })))
.subscribe(({ event, binding }) => {
if (binding) {
const { key, command } = binding;
fire({
key,
command,
preventDefault: () => event.preventDefault(),
stopPropagation: () => event.stopPropagation(),
stopImmediatePropagation: () => event.stopImmediatePropagation(),
cancel: () => {
event.preventDefault();
event.stopImmediatePropagation();
},
});
}
});
}
matchBinding(e, pressedKeys) {
const hasAllModifiers = (modifiers) => {
for (const key of Object.keys(MODIFIERS)) {
const exists = modifiers.some((item) => item === key);
if (e[MODIFIERS[key]] !== exists) {
return false;
}
}
return true;
};
const hasAllValues = (a, b) => R.equals(a, b);
const isMatch = (parts) => {
return hasAllModifiers(parts.modifiers) && hasAllValues(parts.keys, pressedKeys);
};
for (const binding of this.bindings) {
const parts = Keyboard.parse(binding.key);
if (isMatch(parts)) {
return binding;
}
}
return undefined;
}
}