Skip to content

Instantly share code, notes, and snippets.

@andrewplummer
Created January 8, 2017 14:28
Show Gist options
  • Save andrewplummer/52c7a38316303456d13db47e2b732197 to your computer and use it in GitHub Desktop.
Save andrewplummer/52c7a38316303456d13db47e2b732197 to your computer and use it in GitHub Desktop.
// Requires bufferWithMap custom operator (below).
function KeyEventManager (delay) {
this.shortcutsBySeqId = {};
this.partialSequenceCounts = {};
this.delay = delay;
}
KeyEventManager.prototype.connectObservable = function(observable) {
this.setupObservable(observable);
};
KeyEventManager.prototype.fireKeyEvent = function(evt) {
if (!this.fakeKeyEvents) {
this.fakeKeyEvents = new Rx.Subject();
this.setupObservable(this.fakeKeyEvents);
}
this.fakeKeyEvents.onNext(evt);
};
KeyEventManager.prototype.addKeyShortcut = function(keyDescriptors, handler) {
let shortcut = new KeyShortcut(keyDescriptors, handler);
// Only map key commands if the shortcut does not already
// exist, otherwise, allow to overwrite and finish.
if (!this.shortcutsBySeqId[shortcut.id]) {
shortcut.keys.slice(0, -1).reduce((keys, key) => {
keys.push(key);
let sequence = new KeySequence(keys)
this.addPartialSequenceCount(sequence);
return keys;
}, []);
}
this.shortcutsBySeqId[shortcut.sequence.id] = shortcut;
};
KeyEventManager.prototype.getPartialSequenceCount = function(sequence) {
return this.partialSequenceCounts[sequence.id] || 0;
};
KeyEventManager.prototype.addPartialSequenceCount = function(sequence) {
this.partialSequenceCounts[sequence.id] = this.getPartialSequenceCount(sequence) + 1;
};
KeyEventManager.prototype.setupObservable = function(observable) {
observable
.map(evt => Key.fromEvent(evt, true))
.bufferWithMap(sequence => {
if (this.getPartialSequenceCount(sequence) > 0) {
return Rx.Observable.timer(this.delay);
}
return true;
}, (keys) => {
return new KeySequence(keys);
})
.map(sequence => this.shortcutsBySeqId[sequence.id])
.filter(shortcut => shortcut)
.subscribe(shortcut => {
shortcut.handler();
});
};
function KeyShortcut (keyDescriptors, handler) {
this.keys = keyDescriptors.map(d => Key.fromDescriptor(d));
this.handler = handler;
this.sequence = new KeySequence(this.keys);
}
function KeySequence(keys) {
this.id = keys.map(key => key.id).join('|');
}
function Key (id) {
this.id = id;
}
Key.META_KEYS = [
{ name: 'ctrl', eventName: 'ctrlKey', mask: 0x1 },
{ name: 'shift', eventName: 'shiftKey', mask: 0x2 },
{ name: 'option', eventName: 'metaKey', mask: 0x4 },
{ name: 'command', eventName: 'altKey', mask: 0x8 }
];
Key.fromDescriptor = function(descriptor) {
return new Key(Key.getId(descriptor));
};
Key.fromEvent = function(evt) {
return new Key(Key.getId(evt, true));
};
Key.getId = function(obj, isEvent) {
let code = obj[isEvent ? 'keyCode' : 'code'], mask = 0;
for (let i = 0, key; key = Key.META_KEYS[i]; i++) {
let type = key[isEvent ? 'eventName' : 'name']
mask = mask | obj[type] * key.mask;
}
return code.toString() + mask.toString();
};
function KeyEventManager (delay) {
this.shortcutsBySeqId = {};
this.partialSequenceCounts = {};
this.delay = delay;
}
KeyEventManager.prototype.connectObservable = function(observable) {
this.setupObservable(observable);
};
KeyEventManager.prototype.fireKeyEvent = function(evt) {
if (!this.fakeKeyEvents) {
this.fakeKeyEvents = new Rx.Subject();
this.setupObservable(this.fakeKeyEvents);
}
this.fakeKeyEvents.onNext(evt);
};
KeyEventManager.prototype.addKeyShortcut = function(keyDescriptors, handler) {
let shortcut = new KeyShortcut(keyDescriptors, handler);
// Only map key commands if the shortcut does not already
// exist, otherwise, allow to overwrite and finish.
if (!this.shortcutsBySeqId[shortcut.id]) {
shortcut.keys.slice(0, -1).reduce((keys, key) => {
keys.push(key);
let sequence = new KeySequence(keys);
this.addPartialSequenceCount(sequence);
return keys;
}, []);
}
this.shortcutsBySeqId[shortcut.sequence.id] = shortcut;
};
KeyEventManager.prototype.getPartialSequenceCount = function(sequence) {
return this.partialSequenceCounts[sequence.id] || 0;
};
KeyEventManager.prototype.addPartialSequenceCount = function(sequence) {
this.partialSequenceCounts[sequence.id] = this.getPartialSequenceCount(sequence) + 1;
};
KeyEventManager.prototype.setupObservable = function(observable) {
let buffer = [];
observable
.map(evt => {
let key = Key.fromEvent(evt, true);
buffer.push(key);
return new KeySequence(buffer);
})
.switchMap(sequence => {
if (this.getPartialSequenceCount(sequence) > 0) {
return Rx.Observable.timer(this.delay).map(() => {
buffer = [];
return sequence;
});
} else {
buffer = [];
return Rx.Observable.just(sequence);
}
})
.map(sequence => {
return this.shortcutsBySeqId[sequence.id];
})
.filter(shortcut => shortcut)
.subscribe(shortcut => {
shortcut.handler();
});
};
function KeyShortcut (keyDescriptors, handler) {
this.keys = keyDescriptors.map(d => Key.fromDescriptor(d));
this.handler = handler;
this.sequence = new KeySequence(this.keys);
}
function KeySequence(keys) {
this.id = keys.map(key => key.id).join('|');
}
function Key (id) {
this.id = id;
}
Key.META_KEYS = [
{ name: 'ctrl', eventName: 'ctrlKey', mask: 0x1 },
{ name: 'shift', eventName: 'shiftKey', mask: 0x2 },
{ name: 'option', eventName: 'metaKey', mask: 0x4 },
{ name: 'command', eventName: 'altKey', mask: 0x8 }
];
Key.fromDescriptor = function(descriptor) {
return new Key(Key.getId(descriptor));
};
Key.fromEvent = function(evt) {
return new Key(Key.getId(evt, true));
};
Key.getId = function(obj, isEvent) {
let code = obj[isEvent ? 'keyCode' : 'code'], mask = 0;
for (let i = 0, key; key = Key.META_KEYS[i]; i++) {
let type = key[isEvent ? 'eventName' : 'name'];
mask = mask | obj[type] * key.mask;
}
return code.toString() + mask.toString();
};
function KeyEventManager (delay) {
this.shortcuts = new KeyNode();
this.currentNode = this.shortcuts;
this.delay = delay || KeyEventManager.DELAY;
this.timerId = null;
}
KeyEventManager.DELAY = 500;
KeyEventManager.prototype.addKeyShortcut = function(keyDescriptors, handler) {
var shortcut = new KeyShortcut(keyDescriptors, handler);
shortcut.keys.reduce((parentNode, key, i, arr) => {
var node = parentNode.createChildNode(key.id);
if (i === arr.length - 1) {
return node.addHandler(handler);
}
return node;
}, this.shortcuts);
};
KeyEventManager.prototype.fireKeyEvent = function(evt) {
var node = this.currentNode.getChildNode(Key.getId(evt, true));
if (!node) {
// No shortcut exists, so do nothing.
return;
}
if (node.childNodeCount > 0) {
this.currentNode = node;
this.waitForInput();
} else if (node.handler) {
this.fireHandler(node);
}
};
KeyEventManager.prototype.clearKeyEvents = function() {
clearTimeout(this.timerId);
this.timerId = null;
this.currentNode = this.shortcuts;
};
KeyEventManager.prototype.fireHandler = function(node) {
if (node.handler) {
node.handler();
}
this.clearKeyEvents();
};
KeyEventManager.prototype.waitForInput = function() {
if (!this.timerId) {
this.timerId = setTimeout(this.onTimeout.bind(this), this.delay);
}
};
KeyEventManager.prototype.onTimeout = function() {
this.fireHandler(this.currentNode);
};
function KeyNode() {
this.handler = null;
this.childNodes = {};
this.childNodeCount = 0;
}
KeyNode.prototype.addHandler = function(handler) {
this.handler = handler;
};
KeyNode.prototype.getChildNode = function(id) {
return this.childNodes[id];
};
KeyNode.prototype.createChildNode = function(id) {
var childNode = this.childNodes[id];
if (!childNode) {
childNode = new KeyNode();
this.childNodes[id] = childNode;
this.childNodeCount++;
}
return childNode;
};
function KeyShortcut (keyDescriptors, handler) {
this.keys = keyDescriptors.map(d => Key.fromDescriptor(d));
this.handler = handler;
}
function Key (id) {
this.id = id;
}
Key.META_KEYS = [
{ name: 'ctrl', eventName: 'ctrlKey', mask: 0x1 },
{ name: 'shift', eventName: 'shiftKey', mask: 0x2 },
{ name: 'option', eventName: 'metaKey', mask: 0x4 },
{ name: 'command', eventName: 'altKey', mask: 0x8 }
];
Key.fromDescriptor = function(descriptor) {
return new Key(Key.getId(descriptor));
};
Key.fromEvent = function(evt) {
return new Key(Key.getId(evt, true));
};
Key.getId = function(obj, isEvent) {
let code = obj[isEvent ? 'keyCode' : 'code'], mask = 0;
for (let i = 0, key; key = Key.META_KEYS[i]; i++) {
let type = key[isEvent ? 'eventName' : 'name']
mask = mask | obj[type] * key.mask;
}
return code.toString() + mask.toString();
};
(function() {
// Buffer that handles returning observables.
function bufferWithMapAndObservable(bufferFn, mapFn) {
let buffer = [];
function flushBufferWith(mapped) {
buffer = [];
return mapped;
}
return this.switchMap(item => {
buffer.push(item);
let mapped = mapFn(buffer)
let result = bufferFn(mapped);
if (Rx.Observable.isObservable(result)) {
return result.map(() => flushBufferWith(mapped))
} else if (result === true) {
// Flush buffer and return mapped.
return Rx.Observable.just(flushBufferWith(mapped));
} else {
// Return empty and retain buffer.
return Rx.Observable.empty();
}
});
}
Rx.Observable.prototype.bufferWithMap = bufferWithMapAndObservable;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment