Skip to content

Instantly share code, notes, and snippets.

@snj14
Created September 21, 2008 06:58
Show Gist options
  • Save snj14/11847 to your computer and use it in GitHub Desktop.
Save snj14/11847 to your computer and use it in GitHub Desktop.
MultiStrokeShortcutKey.js
// this is shortcut key library that multi stroke key support.
// this library depends on Arrow.js [ http://github.com/motemen/arrow-js/wikis ]
// Key
// 文字列で定義されたショートカットキーとキーイベントの差を吸収して比較できるようにする
function Key(aObject){return (this instanceof Key) ? this.init(aObject) : new Key(aObject)};
// test function
Key.Test = function(){
log(' --- Key.Test --- ');
log(Key('C-x'));
window.addEventListener('keypress', function(e){log(Key(e))}, true);
}
Key.CharCodeTable = {32: 'SPACE'};
Key.KeyCodeTable = {};
for(var name in KeyEvent){if(name.indexOf('DOM_VK_') == 0) Key.KeyCodeTable[KeyEvent[name]] = name.substring(7)};
Key.fromString = function(self, aKeyString){
function hasModifire(aModifier){return !!~aKeyString.indexOf(aModifier)}
self.control = hasModifire('C-');
self.meta = hasModifire('A-') || hasModifire('M-');
self.keyString = aKeyString.replace(/[ACM]-/g,'');
return self;
}
Key.fromEvent = function(self, aEvent){
switch (aEvent.keyCode) {
case aEvent.DOM_VK_CONTROL:
case aEvent.DOM_VK_SHIFT:
case aEvent.DOM_VK_ALT:
return false;
}
self.control = aEvent.ctrlKey;
self.meta = aEvent.altKey;
self.keyString = (Key.KeyCodeTable[aEvent.keyCode] || Key.CharCodeTable[aEvent.charCode] || String.fromCharCode(aEvent.which));
return self;
};
Key.prototype = {
init: function(aObject){
return ((typeof(aObject) == 'string') ? Key.fromString(this, aObject) :
(aObject instanceof KeyEvent) ? Key.fromEvent(this, aObject) :
null);
},
equal: function(aKey){
return ((this.meta == aKey.meta) &&
(this.control == aKey.control) &&
(this.keyString == aKey.keyString))
},
toString: function(){
return ((this.control ? 'C-' : '') +
(this.meta ? 'M-' : '') +
this.keyString);
}
};
// KeyTree
// マルチストロークキーを木構造で保持する
function KeyTree(aKey, aCommand){return (this instanceof KeyTree) ? this.init(aKey, aCommand) : new KeyTree(aKey, aCommand)};
// test function
KeyTree.Test = function(){
log(' --- KeyTree.Test --- ');
var testKeyTree = KeyTree('root');
// add test
testKeyTree.setNodeFromKeyStroke('C-x C-f', function(){log('find-file')});
testKeyTree.setNodeFromKeyStroke('C-x n n', function(){log('narrowing')});
testKeyTree.setNodeFromKeyStroke('C-x n w', function(){log('widen')});
testKeyTree.setNodeFromKeyStroke('C-x C-b', function(){log('list-buffers')});
log(testKeyTree.toStringRecursive());
// => {{{ root }}}}
// [C-x]
// [C-f]
// [n]
// [n]
// [w]
// [C-b]
// remove test
testKeyTree.unsetNodeFromKeyStroke('C-x n w');
log(testKeyTree.toStringRecursive());
// => {{{ root }}}
// [C-x]
// [C-f]
// [n]
// [n]
// [C-b]
}
KeyTree.NodeReferenceError = function(aDescription){
var error = new Error('MultiStrokeShortcutKey.js: ' + aDescription);
error.name = 'KeyTreeReferenceError';
return error;
}
KeyTree.prototype = {
init: function(aKey, aCommand){
this.children = {};
this.parent = null;
this.key = aKey || null;
this.command = aCommand || null;
},
hasChildren: function(){
for(var child in this.children) return true;
return false;
},
getNodeFromKeyStroke: function(aKeyStroke){
var current = this;
aKeyStroke.split(' ').forEach(function(key, index){
current = current.getChild(Key(key));
});
return current;
},
setNodeFromKeyStroke: function(aKeyStroke, aCommand){
var current = this;
aKeyStroke.split(' ').forEach(function(key, index){
current = current.setChild(Key(key));
});
return current.setCommand(aCommand);
},
unsetNodeFromKeyStroke: function(aKeyStroke){
var node = this.getNodeFromKeyStroke(aKeyStroke);
try{
var aStrokeArray = aKeyStroke.split(' ');
var aLastKey = aStrokeArray.pop();
node.getParent().unsetChild(aLastKey);
return true;
}catch(e if e.name == 'KeyTreeReferenceError'){
return false;
}
},
getParent: function(aKey){
var parent = this.parent;
if(!parent) throw(KeyTree.NodeReferenceError(this.toString() + " has not parent key " + aKey.toString()));
return parent;
},
getChild: function(aKey){
var child = this.children[aKey.toString()];
if(!child) throw(KeyTree.NodeReferenceError(this.toString() + " has not child key " + aKey.toString()));
return child;
},
setChild: function(aKey, aCommand){
this.unsetCommand(); // when define C-x C-f, command for C-x is not be available...
var child;
try{
child = this.getChild(aKey);
if(aCommand){
if(child.hasChildren()){
throw(Error(child.toString() + ' has child key already'))
}else{
child.setCommand(aCommand);
}
}
}catch(e if e.name == 'KeyTreeReferenceError'){
child = new KeyTree(aKey, aCommand);
}
child.parent = this;
return (this.children[aKey.toString()] = child);
},
unsetChild: function(aKey){delete this.children[aKey.toString()]},
getCommand: function(){return this.command},
runCommand: function(){return this.command},
setCommand: function(aCommand){return this.command = aCommand},
unsetCommand: function(){return this.command = null},
getKey: function(){return this.key},
setKey: function(aKey){return (aKey instanceof Key) ? (this.key = aKey) : false},
mapChildren: function(aFunction){
var res = [];
for(var key in this.children) res.push(aFunction(this.children[key]));
return res;
},
toString: function(){ // for debug
return (this.key ? ('[' + this.key.toString() + ']') : '');
},
toStringRecursive: function(depth){ // for debug
depth = depth || 0;
return ((new Array(depth)).join(' ') +
((depth == 0) ? (' {{{' + this.key.toString() + '}}}\n') : ('[' + this.key.toString() + ']\n')) +
this.mapChildren(function(child){return child.toStringRecursive(depth + 1)}).join(''));
}
};
// ShortcutKey
// KeyTreeとイベントリスナの橋渡し
function ShortcutKey(aObject){return (this instanceof ShortcutKey) ? this.init(aObject) : new ShortcutKey(aObject)};
// test function
ShortcutKey.Test = function(){
log(' --- ShortcutKey.Test --- ');
var ShortcutKeyOnWindow = ShortcutKey(window);
// ShortcutKeyOnWindow.debug = true;
ShortcutKeyOnWindow.addKey('C-x C-f', function(){log('find-file')});
ShortcutKeyOnWindow.addKey('C-x n n', function(){log('narrowing')});
ShortcutKeyOnWindow.addKey('C-x n w', function(){log('widen')});
ShortcutKeyOnWindow.addKey('C-x C-b', function(){log('select buffer')});
var count = 0;
ShortcutKeyOnWindow.addKey('j', function(){log('! (',++count,')')});
ShortcutKeyOnWindow.removeKey('C-x n w', function(){log('widen')});
log(ShortcutKeyOnWindow.Root.toString());
log(ShortcutKeyOnWindow.Root.toStringRecursive());
}
ShortcutKey.Hash = {};
ShortcutKey.prototype = {
init: function(aObject){
var aKey = '' + aObject;
var self = this;
if((self = ShortcutKey.Hash[aKey])){
return self;
}
ShortcutKey.Hash[aKey] = this;
this.debug = false;
this.previousEventType = 'none';
this.currentEventType = 'none';
this.previousEventTime = 0;
this.currentEventTime = 0;
this.previousTimer = null;
this.noexec = false;
this.interval = 400; // 400 ms
this.initState();
this.Root = KeyTree('root');
this.currentRoot = this.Root;
this.currentKeyTree = this.Root;
this.currentCommand = null;
var keypress = (
(
((this.KeyWait(aObject, 'keydown'))['>>>'](this.KeyIsKeydown))
['<+>']
(this.KeyWait(aObject, 'keypress'))
)
['>>>']
(
((this.KeyIsNotAvailable)['>>>'](this.WaitRootKey))
['<+>']
((this.CommandIsAvailable)['>>>'](this.RunCommand)['>>>'](this.WaitRootKey))
['<+>']
((this.NextKeyIsAvailable)['>>>'](this.WaitNextKey))
)
);
keypress.loop().run();
return this;
},
addKey: function(aKeyStroke, aCommand){return this.Root.setNodeFromKeyStroke(aKeyStroke, aCommand)},
removeKey: function(aKeyStroke){return this.Root.unsetNodeFromKeyStroke(aKeyStroke)},
initState: function(){
var self = this;
this.KeyIsKeydown = Arrow.fromCPS(function(aKey, k){});
this.KeyIsNotAvailable = Arrow.fromCPS(function(aKey, k){
if(!(self.currentKeyTree = self.currentRoot.getChild(aKey))){
if(self.debug) log('KeyIsNotAvailable',self.currentEventType);
k(aKey);
}
});
this.CommandIsAvailable = Arrow.fromCPS(function(aKey, k){
if(self.currentKeyTree){
var cmd = self.currentKeyTree.getCommand()
if(cmd) {
if(self.debug) log('CommandIsAvailable',self.currentEventType)
k(cmd);
}
}
});
this.NextKeyIsAvailable = Arrow.fromCPS(function(x, k){
if(self.currentKeyTree){
if(self.currentKeyTree.hasChildren()){
if(self.debug) log('NextKeyIsAvailable',self.currentEventType)
k(x);
}
}
});
this.WaitNextKey = Arrow.fromCPS(function(x, k){
self.currentRoot = self.currentKeyTree
k(x);
});
this.WaitRootKey = Arrow.fromCPS(function(x, k){
self.currentRoot = self.Root
k(x);
});
this.RunCommand = Arrow.fromCPS(function(aCommand, k){
if((self.previousEventType == 'keydown') || !self.noexec){
self.currentEventTime = (new Date()).getTime();
self.noexec = false;
}
if((self.previousEventType == 'keydown') && self.previousEventTime){
interval = Math.floor((self.currentEventTime - self.previousEventTime) * 0.9);
if(interval < 1500){
if((self.interval != interval) && (self.interval < 220) && (interval < 200)){ // 200ms以下の連打は40msまで少しずつ速くしていく
interval = Math.max(40, Math.floor(self.interval * 0.9)); // 40ms ... (1 / 24)s [ http://www.slideshare.net/otsune/20-266079/ ]
}
self.interval = interval
}
}
if(!self.noexec){
self.noexec = true
if(self.previousTimer) clearTimeout(self.previousTimer);
self.previousTimer = setTimeout(function(){
self.previousTimer = null;
self.noexec = false;
if(self.debug) log('now executable!');
}, self.interval);
self.previousEventTime = self.currentEventTime;
aCommand();
}
k();
});
this.KeyWait = function(object, event){
return Arrow.fromCPS(function(x, k) {
var stop = false;
var listener = function(aEvent) {
if (stop) return;
stop = true;
self.previousEventType = self.currentEventType;
self.currentEventType = aEvent.type;
if(self.debug) log(aEvent.type, '---')
k(Key(aEvent));
};
Arrow.Compat.addEventListener(object, event, listener, true);
this.cancel = function(){stop = true};
});
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment