Created
September 21, 2008 06:58
-
-
Save snj14/11847 to your computer and use it in GitHub Desktop.
MultiStrokeShortcutKey.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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