UNPKG

@platform/react

Version:

React refs and helpers.

172 lines (171 loc) 6.63 kB
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; } }