Skip to content
This repository has been archived by the owner on Nov 8, 2017. It is now read-only.

support for onRelease listener and preventing automatic key repeats #55

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 14 additions & 18 deletions README-DETAILED.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@ In other words, the following are the same:
jwerty.key
==========

jwerty.key(jwertyCode, callbackFunction, [callbackContext]);
jwerty.key(jwertyCode, callbackFunction, callbackContext, selector, selectorContext, onRelease);

`jwerty.key` will attach an event listener and fire `callbackFunction` when
`jwertyCode` matches. The event listener is attached to `document`, meaning
it will listen for any key events on the page (a global shortcut listener). If
`jwertyCode` matches. If `selector` is not given, the event listener is attached to `document`,
meaning it will listen for any key events on the page(a global shortcut
listener). If `selector` is given, an element target will be saught instead.
`selectorContext`, if given, is used to search for `selector` within
`selectorContext`, similar to jQuery's `$('selector', 'context')`. If
`callbackContext` is specified then it will be supplied as
`callbackFunction`'s context - in other words, the keyword `this` will be
set to `callbackContext` inside the `callbackFunction` function.
Expand All @@ -72,20 +75,13 @@ set to `callbackContext` inside the `callbackFunction` function.
key, for example: `jwerty.key('ctrl+V', false)` will disable ctrl+V's default
behaviour.


jwerty.key(jwertyCode, callbackFunction, [callbackContext], [selector, [selectorContext]]);

`jwerty.key` will attach an event listener and fire `callbackFunction` when
`jwertyCode` matches. The event listener is attached to `selector`.
`callbackContext` can be ommited if not needed, and `selector` becomes
the third argument. `selectorContext` is used to search for `selector`
within `selectorContext`, similar to jQuery's `$('selector', 'context')`.

- `selector` can be a CSS1/2/3 selector - it will use
document.querySelectorAll, unless you have jQuery, Zepto or Ender installed,
in which case it will use those as the selector engine.

- `selector` can be a DOM element (such as HTMLDivElement), or a jQuery
element object, or a Zepto element object, or an Ender element object.

- `selectorContext` has the same rules as `selector`, it can be a
string, DOM element or jQuery/Zepto/Ender element object.

Expand Down Expand Up @@ -116,23 +112,23 @@ jwerty.key('ctrl+shift+p', false, '#myInput');
jwerty.event
==========

jwerty.event(jwertyCode, callbackFunction, [callbackContext]);
jwerty.event(jwertyCode, onFire, callbackContext, [onRelease]);

`jwerty.event` will return a function, which expects the first argument to be a
key event. When the key event matches `jwertyCode`, `callbackFunction`
key event. When the key event matches `jwertyCode`, `onFire`
is fired. `jwerty.event` is used by `jwerty.key` to bind the function it returns.
`jwerty.event` is useful for attaching to your own event listeners. It can be
used as a decorator method to encapsulate functionality that you only want to
fire after a specific key combo. If `callbackContext` is specified then it
will be supplied as `callbackFunction`'s context - in other words, the
will be supplied as `onFire`'s context - in other words, the
keyword `this` will be set to `callbackContext` inside the
`callbackFunction` function.
`onFire` function.

- If `callbackFunction` returns `false` then preventDefault() will be
- If `onFire` returns `false` then preventDefault() will be
called for the event, in other words - what the browser normally does when
this key is pressed will not happen.

- If `callbackFunction` can be a boolean (`true` or `false`), rather than an
- If `onFire` can be a boolean (`true` or `false`), rather than an
actual function. If it is a boolean, it will be treated like a function that
instantly returns that value. This is useful if you just want to disable a
key, for example: `jwerty.key('ctrl+V', false)` will disable ctrl+V's default
Expand Down
153 changes: 97 additions & 56 deletions jwerty.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,43 @@
$b, // Event binding function
$u, // Event unbinding function
$f, // Event firing function
ke = 'keydown';

kdstring = 'keydown';

// function actuallyInstanceOf(v, constructor){
// return (v == null && constructor == null) ||
// v.constructor == constructor || //for the primitives who've no prototype chain
// v instanceof constructor; //for the rest
// }
// In case you need to check the type heirarchy, uncomment the above function and refactor realTypeOf out in its favor.
function realTypeOf(v, s) {
return (v === null) ? s === 'null'
: (v === undefined) ? s === 'undefined'
: (v.is && v instanceof $) ? s === 'element'
: Object.prototype.toString.call(v).toLowerCase().indexOf(s) > 7;
}


if ($ === $d) {
$$ = function (selector, context) {
return selector ? $.querySelector(selector, context || $) : $;
};
$b = function (e, fn) { e.addEventListener(ke, fn, false); };
$u = function (e, fn) { e.removeEventListener(ke, fn, false); };
$b = function (e, fn, eventName /*?*/) { e.addEventListener(eventName || kdstring, fn, false); };
$u = function (e, fn, eventName /*?*/) { e.removeEventListener(eventName || kdstring, fn, false); };
$f = function (e, jwertyEv) {
var ret = $d.createEvent('Event'),
i;

ret.initEvent(ke, true, true);
ret.initEvent(kdstring, true, true);

for (i in jwertyEv) ret[i] = jwertyEv[i];

return (e || $).dispatchEvent(ret);
};
} else {
$$ = function (selector, context) { return $(selector || $d, context); };
$b = function (e, fn) { $(e).bind(ke + '.jwerty', fn); };
$u = function (e, fn) { $(e).unbind(ke + '.jwerty', fn) };
$f = function (e, ob) { $(e || $d).trigger($.Event(ke, ob)); };
$b = function (e, fn, eventName /*?*/) { $(e).bind((eventName || kdstring) + '.jwerty', fn); };
$u = function (e, fn, eventName /*?*/) { $(e).unbind((eventName || kdstring) + '.jwerty', fn) };
$f = function (e, ob) { $(e || $d).trigger($.Event(kdstring, ob)); };
}

// Private
Expand Down Expand Up @@ -343,30 +350,33 @@
*
* `jwerty.event` will return a function, which expects the first
* argument to be a key event. When the key event matches `jwertyCode`,
* `callbackFunction` is fired. `jwerty.event` is used by `jwerty.key`
* `onFire` is fired. `jwerty.event` is used by `jwerty.key`
* to bind the function it returns. `jwerty.event` is useful for
* attaching to your own event listeners. It can be used as a decorator
* method to encapsulate functionality that you only want to fire after
* a specific key combo. If `callbackContext` is specified then it will
* be supplied as `callbackFunction`'s context - in other words, the
* be supplied as `onFire`'s context - in other words, the
* keyword `this` will be set to `callbackContext` inside the
* `callbackFunction` function.
* `onFire` function.
*
*
* @param {Mixed} jwertyCode can be an array, or string of key
* combinations, which includes optinals and or sequences
* @param {Function} callbackFucntion is a function (or boolean) which
* @param {Function} onFire is a function (or boolean) which
* is fired when jwertyCode is matched. Return false to
* preventDefault()
* @param {Object} callbackContext (Optional) The context to call
* `callback` with (i.e this)
* @param {Function} onRelease is a function called when the user lets
* go of one of the keys in the final keyCode of the sequence.
*
*/
event: function (jwertyCode, callbackFunction, callbackContext /*? this */) {

// Construct a function out of callbackFunction, if it is a boolean.
if (realTypeOf(callbackFunction, 'boolean')) {
var bool = callbackFunction;
callbackFunction = function () { return bool; };
event: function (jwertyCode, onFire, callbackContext /*? this */, onRelease /*?*/) {
// Construct a function out of onFire, if it is a boolean.
if (realTypeOf(onFire, 'boolean')) {
var bool = onFire;
onFire = function () { return bool; };
}

jwertyCode = new JwertyCode(jwertyCode);
Expand All @@ -375,38 +385,66 @@
var i = 0,
c = jwertyCode.length - 1,
returnValue,
jwertyCodeIs;

jwertyCodeIs,
keysHeld = false;

var keyUpListener = function(ev){
/* this is... heuristic. Ideally the keyUpListener would check to see that
* the key being released actually undoes the combo that triggered the current
* step of the sequence. That would take a lot of support code, especially when
* you need to start tracking which optional fired the event. Not worth it,
* considering that this will work in 98% of cases. */
keysHeld = false;
if (i == c && onRelease) { //onRelease only fires when this is the last in the sequence, just like onFire
onRelease(ev);
}
/*unbinds itself each time it is triggered because otherwise we cannot ensure it
*will be unbound by users handling the event binding themselves*/
$u( document, keyUpListener, 'keyup' ); /*(( it had been bound to document
* because we don't actually want it to be specific about where in the document
* the key is released, if a key is released anywhere, the combo probably doesn't
* hold any more. Also, there's no way of knowing what element the event callback
* we're building here is going to be bound to anyway :P ))*/
};

// This is the event listener function that gets returned...
return function (event) {

// if jwertyCodeIs returns truthy (string)...
if ((jwertyCodeIs = jwerty.is(jwertyCode, event, i))) {
// ... and this isn't the last key in the sequence,
// incriment the key in sequence to check.
if (i < c) {
++i;
return;
// ... and this is the last in the sequence (or the only
// one in sequence), then fire the callback
} else {
returnValue = callbackFunction.call(
callbackContext || this, event, jwertyCodeIs);

// If the callback returned false, then we should run
// preventDefault();
if (returnValue === false) event.preventDefault();

// Reset i for the next sequence to fire.
i = 0;
return;
return function (ev) {
//if the incoming event is the same as the last expected combo, we will not accept another firing until we get a keyUp, as such a firing would be indicative of an automatic repeat input, and that isn't always wanted
var previousIndex = i == 0 ? 0 : i - 1;
if ( !keysHeld || !jwerty.is(jwertyCode, ev, previousIndex) ){ /*We DO accept a
* firing without a keyRelease in the case that the previous event is different
* to the last, as, say, for the sequence, ctrl+a,ctrl+b,ctrl+a,ctrl+c,ctrl+u,ctrl+s,
* requring the user to release the the last letter key before putting the next will
* unnecessarily slow their typing. */
// if jwertyCodeIs returns truthy (string)...
if ((jwertyCodeIs = jwerty.is(jwertyCode, ev, i))) {
// ... and this isn't the last key in the sequence,
//key press begins
keysHeld = true;
$b( document, keyUpListener, 'keyup' );
// increment the key in sequence to check.
if (i < c) {
++i;
// ... or if is the last in the sequence (or the only
// one in sequence), then fire the callback
} else {
returnValue = onFire.call(
callbackContext || this, ev, jwertyCodeIs);

//If the callback returned false, then we should run preventDefault();
if (returnValue === false) ev.preventDefault();

// Reset i for the next sequence to fire.
i = 0;
}
}else{
// If the event that fired was not the one we were expecting
// , we should reset i to 0.
// unless this combo matches the first in the sequence,
// in which case we should reset i to 1.
i = jwerty.is(jwertyCode, ev) ? 1 : 0;
}
}

// If the event didn't hit this time, we should reset i to 0,
// that is, unless this combo was the first in the sequence,
// in which case we should reset i to 1.
i = jwerty.is(jwertyCode, event) ? 1 : 0;
};
},

Expand Down Expand Up @@ -456,23 +494,25 @@
}
return returnValue;
},


/**
* jwerty.key
*
* `jwerty.key` will attach an event listener and fire
* `callbackFunction` when `jwertyCode` matches. The event listener is
* attached to `document`, meaning it will listen for any key events
* `onFire` when `jwertyCode` matches. `onRelease` will be fired when
* a key is released after the matching combo is inputted.
* The event listener is attached to `document`, meaning it will listen for any key events
* on the page (a global shortcut listener). If `callbackContext` is
* specified then it will be supplied as `callbackFunction`'s context
* specified then it will be supplied as `onFire`'s context
* - in other words, the keyword `this` will be set to
* `callbackContext` inside the `callbackFunction` function.
* `callbackContext` inside the `onFire` function.
* returns a subscription handle `h`, by which you may undo the binding
* by calling `h.unbind()`
*
* @param {Mixed} jwertyCode can be an array, or string of key
* combinations, which includes optinals and or sequences
* @param {Function} callbackFunction is a function (or boolean) which
* @param {Function} onFire is a function (or boolean) which
* is fired when jwertyCode is matched. Return false to
* preventDefault()
* @param {Object} callbackContext (Optional) The context to call
Expand All @@ -481,9 +521,10 @@
* or an HTML*Element on which to bind the eventListener
* @param {Mixed} selectorContext can be a string, jQuery/Zepto/Ender
* object, or an HTML*Element on which to scope the selector
*
* @param {Function} onRelease will be called when the user lets go of
* a key after this binding has been triggered
*/
key: function (jwertyCode, callbackFunction, callbackContext /*? this */, selector /*? document */, selectorContext /*? body */) {
key: function (jwertyCode, onFire, callbackContext /*? this */, selector /*? document */, selectorContext /*? body */, onRelease /*?*/) {
// Because callbackContext is optional, we should check if the
// `callbackContext` is a string or element, and if it is, then the
// function was called without a context, and `callbackContext` is
Expand All @@ -499,10 +540,10 @@
// If `realSelector` is already a jQuery/Zepto/Ender/DOM element,
// then just use it neat, otherwise find it in DOM using $$()
var element = realTypeOf(realSelector, 'element') ? realSelector : $$(realSelector, realSelectorContext);
var callback = jwerty.event(jwertyCode, callbackFunction, realcallbackContext);
var callback = jwerty.event(jwertyCode, onFire, realcallbackContext, onRelease);
$b( element, callback );

return {unbind:function(){ $u( element, callback ) }};
return {unbind:function(){ $u( element, callback ); }};
},

/**
Expand Down
Loading