Skip to content

Instantly share code, notes, and snippets.

@SignpostMarv
Created September 27, 2013 15:48
Show Gist options
  • Save SignpostMarv/6730726 to your computer and use it in GitHub Desktop.
Save SignpostMarv/6730726 to your computer and use it in GitHub Desktop.
trigger a function with a given key sequence
/**
* @license License and Terms of Use
*
* Copyright (c) 2013 SignpostMarv
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Examples:
keySequenceTrigger(
'one-time-only',
function(){
console.log('KTHXBAI!');
// keySequenceTrigger calls the trigger function with handler as the
// this argument, enabling minimalist removal logic.
keySequenceTrigger.remove(this);
}
);
keySequenceTrigger({
sequence : '↑↑↓↓←→←→ba',
trigger : function(){
console.log('KONAMI CODE!');
}
});
keySequenceTrigger({
sequence : '⎈⎇⇑h', // ctrl, alt, shift, h
trigger : function(){
console.log('HIPPOS!');
}
});
*/
(function(window, undefined){
'use strict';
var
ArrayProt = window['Array'].prototype,
callBacks = {},
sequenceCounters = {},
addEvent = isIn(document, 'addEventListener')
? function(a,b,c){ a.addEventListener(b, c, false); }
: function(a,b,c){ a.addEvent(b, c); }
;
function isIn(obj, prop){
return (prop in obj);
}
function hasOwn(obj, prop){
for(var i=1;i<arguments.length;++i){
if(!obj.hasOwnProperty(prop)){
return false;
}
}
return true;
}
if(!hasOwn(ArrayProt, 'forEach')){
ArrayProt['forEach'] = function(cb){
for(var i=0;i<this.length;++i){
cb(this[i], i, this);
}
}
}
function sequenceModifier(sequenceIn){
var
sequenceOut = sequenceIn,
replacements = {
'⇑' : 16, '⇑' : 16, // shift key
'⎈' : 17, '⎈' : 17, // ctrl key
'⎇' : 18, '⎇' : 18, // alt key
'←' : 37, 'â†' : 37,
'↑' : 38, '↑' : 38,
'→' : 39, '→' : 39,
'↓' : 40, '↓' : 40,
}
;
for(var i in replacements){
if(hasOwn(replacements, i)){
sequenceOut = sequenceOut.replace(
new RegExp(i, 'g'),
String.fromCharCode(replacements[i])
);
}
}
return sequenceOut.toLowerCase();
}
function keySequenceTrigger(opts){
// assignment
var
self = this,
sequence, trigger, element, sequenceIn
;
if(arguments.length == 0){
throw new Error('No arguments supplied');
}else if(arguments.length == 1){
if(typeof(opts) != 'object'){
throw new Error('One argument supplied, but not as object!');
}else{
sequence = opts.sequence;
trigger = opts.trigger;
element = opts.element;
}
}else if(arguments.length >= 2){
sequence = arguments[0];
trigger = arguments[1];
element = arguments[2]
}
element = element || document;
// validation
if(typeof(sequence) != 'string'){
throw new Error('Sequence must be supplied as string!');
}else if(typeof(trigger) != 'function'){
throw new Error('Trigger must be supplied as function!');
}else if(
!isIn(element, 'addEventListener')
&& !isIn(element, 'attachEvent')
){
throw new Error('Element must support event attachment!');
}
sequenceIn = sequence;
sequence = sequenceModifier(sequence);
if(!hasOwn(callBacks, sequence)){
callBacks[sequence] = [];
}
if(!hasOwn(sequenceCounters, sequence)){
sequenceCounters[sequence] = 0;
}
var handler = function(){trigger.call(handler)};
handler.sequenceIn = sequenceIn;
handler.sequence = sequence;
handler.element = element;
callBacks[sequence].push(handler);
}
keySequenceTrigger.remove = function(){
var
removeThese = {}
;
for(var i=0;i<arguments.length;++i){
if(hasOwn(arguments[i], 'sequence')){
var
handler = arguments[i],
sequence = handler.sequence
;
if(hasOwn(callBacks, sequence)){
var pos = callBacks[sequence].indexOf(handler);
if(pos >= 0){
if(!hasOwn(removeThese, sequence)){
removeThese[sequence] = [];
}
removeThese[sequence].push(pos);
}
}
}
}
for(var sequence in removeThese){
removeThese[sequence].sort();
removeThese[sequence].reverse();
removeThese[sequence].forEach(function(e){
callBacks[sequence].splice(e, 1);
});
}
}
function keyEventHandler(e){
var
e = e || window.event
;
if(e.type == 'down' && (e.keyCode < 37 || e.keyCode > 40)){
return; // we only want keydown for arrow keys
}else if(
(e.keyCode < 65 || e.keyCode > 90) // ignore upper-case
&& (e.keyCode < 189) // ignore duplicate events
){
var
callThese = []
;
for(var i in sequenceCounters){
if(
hasOwn(sequenceCounters, i)
&& hasOwn(callBacks, i) // callback check
){
if(e.keyCode == i.charCodeAt(sequenceCounters[i])){
++sequenceCounters[i];
if(sequenceCounters[i] >= i.length){
callBacks[i].forEach(function(f){
if(e.target == f.element || (f.element == document && e.target == document.body)){
callThese.push(f);
}
});
sequenceCounters[i] = 0;
}
}else{
sequenceCounters[i] = 0;
}
}
}
callThese.forEach(function(f){
f();
});
}
}
['keypress', 'keydown'].forEach(function(e){
addEvent(document, e, keyEventHandler);
});
window['keySequenceTrigger'] = keySequenceTrigger;
})(window);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment