keymaster
keymaster copied to clipboard
Key Sequences
I'm writing this to start a "high" level discussion about how to implement this.
I'm looking to implement similar functionality to that of GMail. Allowing me to have a global first-press, followed by an action.
IE: I want to be able to press g then h, to go home. However at present this doesn't seem possible with keymaster.
I currently implement something similar by using setScope and having a scope specific for my "go actions", however if I'm already in a scope, and these are intended to be global actions, then it gets difficult to return to the previous scope.
My suggestion is a proposed setTemporaryScope method, whereby it automatically returns to the previous scope after next keypress, or if there is no keypress within a certain time frame (say 1second) then it'll return to the previous scope after time out.
Thoughts?
I like this, but am not sure about the best API for it yet. It might not be the best way to just sequence up shortcuts, and I feel a full (VIM-style) system of keyboard commands is beyond the scope of Keymaster. I will give it some thought!
Just an idea:
key.twostep('g', [
['h', function(){
console.log("home")
}],
['s', function(){
console.log("settings")
}]
]);
key("g",key.when({
'h':function(){
console.log("Home");
},
's':key.when("i",function(){
console.log("Settings -> Issues")
},function(){
console.log("Settings");
})
}))
Where key.when returns a function that binds the even to the given key(s) and unbinds them after they are triggered or a time runs out. You could also pass a callback for when the time runs out. (this could use scope internally). This could be written as a plugin instead of putting it in the core
how about:
key.sequence(["g", "h"], function () { console.log("go home"); });
key.sequence(["s", "f", "s"], function () {console.log("Settings -> Fonts -> Size"); });
The difficult part of having this functionality comes when a key press or sequence is part of a larger sequence, e.g. if you have this together with the code above:
key("s", function() {console.log("Settings"); });
// or
key.sequence(["s", "f"], function () {console.log("Settings -> Fonts"); });
Not sure if that's a situation that would come up often.
The following code works in Chrome, haven't tested it yet in other browsers. IE will fail due to use of Function.bind, so a polyfill will be needed.
(function (keymaster) {
keymaster._seqScope = "seq_";
keymaster.sequence = function (keys, scope, method) {
if (method == undefined) {
method = scope;
scope = 'all';
}
for(i = 0; i < keys.length; i++) {
if (i < keys.length-1) {
//create specific scope for current key in sequence
_seqScope = _seqScope + keys[i];
assignKey(keys[i], scope, function (ev, key) {
setScope(this.toString());
// reset scope after 1 second
_timer = setTimeout(function () {
setScope('all');
}, 1000);
}.bind(_seqScope));
} else {
// last key should perform the method
assignKey(keys[i], _seqScope, method);
}
scope = _seqScope;
}
// reset _seqScope for new sequence
_seqScope = "seq_";
}
})(key);
I need to test the plugin setup, and of course it should be tested for robustness in various browsers.
A quick draft of how my idea would work as a plug-in (it will likely fail when scoping is used)
(function(keymaster){
keymaster.when=function(key, fn, callback, timeout){
timeout||(timeout = 1000)
return function(){
var called=0;
timeout&&setTimeout(function(){
called=1;
call=typeof key=="object"?
fn
:
callback;
call&&call();
}, timeout)
if(typeof key=="object"){
for(var x in key){
(function(key,fn){
keymaster(key, function(){
if(!called){
fn.apply(this.arguments);
called=1;
}
})
}(x,key[x]))
}
}
else
{
keymaster(key, function(){
if(!called){
fn.apply(this.arguments);
called=1;
}
})
}
}
}
})(key);
How about a space?
key('⌘+a d', function(){ });
I decided to take a crack at this and wrapped it up into a sorta plugin for keymaster, since I'm not sure this belongs in core. Anyway, here it is: https://github.com/cmawhorter/keymaster-sequences.js
Another lib I looked at used spaces too, as @paales recommended, so I decided to go with that.
key.sequence('ctrl+a d', function(){});
Demo: http://jsfiddle.net/ggZxB/1/
late in the game, but here is how i solved it:
const sequence = (mapping, scope, fn, timeout) => {
mapping.split(',').map(s => s.trim()).forEach(map => {
const parts = mapping.split(' ').map(s => s.trim())
parts.reduce((currentScope, part, index) => {
const subScope = `${currentScope}_${part}`
key(part, currentScope, (...args) => {
if(index == 0 && parts.length > 1)
setTimeout( () => { key.setScope(scope) }, timeout)
}
if(index == parts.length -1){
key.setScope(scope)
fn(args)
} else {
key.setScope(subScope)
}
})
return subScope
}, scope)
})
}
a mapping like this: sequence('a b c d', 'foo', alert('done'), 2000) will generate 3 mappings like this: key('a', 'myscope', () => { setTimeout( () => { key.setScope('myscope') }, 2000); setScope('myscope_a') }) key('b', 'myscope_a', () => { setScope('myscope_sequence_a_b') }) key('c', 'myscope_a_b', () => { setScope('myscope_sequence_a_b_c') }) key('d', 'myscope_a_b_c', () => { alert('done') })