Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event delegation, sharing document bound handlers #356

Open
wants to merge 4 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
122 changes: 86 additions & 36 deletions mousetrap.js
Expand Up @@ -182,6 +182,45 @@
object.attachEvent('on' + type, callback);
}

// Mousetrap instances map->list for event delegation
var mousetraps = {};

/**
* handles a keydown event
*
* @param {Event} e
* @returns void
*/
function _handleKeyEvent(e) {

// normalize e.which for key events
// @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
if (typeof e.which !== 'number') {
e.which = e.keyCode;
}

var character = _characterFromEvent(e);

// no character found then stop
if (!character) {
return;
}

var mods = _eventModifiers(e);
var element = e.target;
while (element) {
var traps = mousetraps[_mousetrapElementId(element)];
if (traps) traps.forEach(function(trap) {
trap.handleKey(character, mods, e);
});

if (element === document) {
return;
}
element = element.parentNode
}
}

/**
* takes the event and returns the key character
*
Expand Down Expand Up @@ -432,6 +471,21 @@
return _belongsTo(element.parentNode, ancestor);
}

var uniqueId = 1;

function _mousetrapElementId(element) {
if (element === document) {
return '#doc';
}

var id = element.getAttribute('data-mousetrap-id');
if (!id) {
id = uniqueId++;
element.setAttribute('data-mousetrap-id', id);
}
return id;
}

function Mousetrap(targetElement) {
var self = this;

Expand Down Expand Up @@ -628,6 +682,12 @@
* @returns void
*/
self._handleKey = function(character, modifiers, e) {
// need to use === for the character check because the character can be 0
if (e.type == 'keyup' && _ignoreNextKeyup === character) {
_ignoreNextKeyup = false;
return;
}

var callbacks = _getMatches(character, modifiers, e);
var i;
var doNotReset = {};
Expand Down Expand Up @@ -707,36 +767,6 @@
_ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
};

/**
* handles a keydown event
*
* @param {Event} e
* @returns void
*/
function _handleKeyEvent(e) {

// normalize e.which for key events
// @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
if (typeof e.which !== 'number') {
e.which = e.keyCode;
}

var character = _characterFromEvent(e);

// no character found then stop
if (!character) {
return;
}

// need to use === for the character check because the character can be 0
if (e.type == 'keyup' && _ignoreNextKeyup === character) {
_ignoreNextKeyup = false;
return;
}

self.handleKey(character, _eventModifiers(e), e);
}

/**
* called to set a 1 second timeout on the specified sequence
*
Expand Down Expand Up @@ -883,12 +913,18 @@
for (var i = 0; i < combinations.length; ++i) {
_bindSingle(combinations[i], callback, action);
}
};

// start!
_addEvent(targetElement, 'keypress', _handleKeyEvent);
_addEvent(targetElement, 'keydown', _handleKeyEvent);
_addEvent(targetElement, 'keyup', _handleKeyEvent);
// activate
var id = _mousetrapElementId(self.target);
var traps = mousetraps[id];
if (!traps) {
mousetraps[id] = traps = [];
}
var idx = traps.indexOf(self);
if (idx === -1) {
traps.push(self);
}
};
}

/**
Expand Down Expand Up @@ -960,6 +996,15 @@
var self = this;
self._callbacks = {};
self._directMap = {};

// remove from the active instance list
var traps = mousetraps[_mousetrapElementId(self.target)];
if (traps) {
var idx = traps.indexOf(self);
if (idx !== -1) {
traps.splice(idx, 1);
}
}
return self;
};

Expand Down Expand Up @@ -989,7 +1034,7 @@
/**
* exposes _handleKey publicly so it can be overwritten by extensions
*/
Mousetrap.prototype.handleKey = function() {
Mousetrap.prototype.handleKey = function(character) {
var self = this;
return self._handleKey.apply(self, arguments);
};
Expand Down Expand Up @@ -1023,6 +1068,11 @@
} (method));
}
}

// start!
_addEvent(document, 'keypress', _handleKeyEvent);
_addEvent(document, 'keydown', _handleKeyEvent);
_addEvent(document, 'keyup', _handleKeyEvent);
};

Mousetrap.init();
Expand Down
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -23,8 +23,9 @@
"gitHead": "c202a0bd4967d5a3064f9cb376db51dec9345336",
"readmeFilename": "README.md",
"devDependencies": {
"grunt": "~0.4.1",
"grunt": "^1.0.1",
"grunt-cli": "^1.2.0",
"grunt-complexity": "~0.1.2",
"grunt-mocha": "~0.3.1"
"grunt-mocha": "^1.0.2"
}
}
57 changes: 57 additions & 0 deletions tests/test.mousetrap.js
Expand Up @@ -669,6 +669,18 @@ describe('wrapping a specific element', function() {
expect(spy.callCount).to.equal(0, 'callback should not have fired');
});

it('z key does not fire after #reset() call', function() {
var spy = sinon.spy();

var mousetrap = new Mousetrap(form);
mousetrap.bind('z', spy);
KeyEvent.simulate('Z'.charCodeAt(0), 90, [], textarea); // through
mousetrap.reset();
KeyEvent.simulate('Z'.charCodeAt(0), 90, [], textarea); // neglected

expect(spy.callCount).to.equal(1, 'callback should only have fired before #reset');
});

it('should work when constructing a new mousetrap object', function() {
var spy = sinon.spy();

Expand All @@ -694,6 +706,51 @@ describe('wrapping a specific element', function() {
expect(spy.args[0][0]).to.be.an.instanceOf(Event, 'first argument should be Event');
expect(spy.args[0][1]).to.equal('a', 'second argument should be key combo');
});

it('should fire for all Mousetrap instances on the target', function() {
var spy = sinon.spy();

var mousetrap = new Mousetrap();
mousetrap.bind('a', spy);
mousetrap = new Mousetrap();
mousetrap.bind('a', spy);

KeyEvent.simulate('a'.charCodeAt(0), 65);

expect(spy.callCount).to.equal(2, 'callback should for all instances');
expect(spy.args[0][0]).to.be.an.instanceOf(Event, 'first argument should be Event');
expect(spy.args[0][1]).to.equal('a', 'second argument should be key combo');
});

it('should fire for all Mousetrap instances in hierarchy', function() {
var spy = sinon.spy();

var mousetrap = new Mousetrap(document.body);
mousetrap.bind('a', spy);
mousetrap = new Mousetrap(form);
mousetrap.bind('a', spy);

KeyEvent.simulate('a'.charCodeAt(0), 65, [], textarea);

expect(spy.callCount).to.equal(2, 'callback should for all instances');
});

it('a key does not fire after #reset() call in hierarchy', function() {
var spy = sinon.spy();

var mousetrap = new Mousetrap(document.body);
mousetrap.bind('a', spy);

// bound & reset Mousetrap instance
mousetrap = new Mousetrap(form);
mousetrap.bind('a', spy);
mousetrap.reset();

KeyEvent.simulate('a'.charCodeAt(0), 65, [], textarea);

expect(spy.callCount).to.equal(1, 'callback should only have fired before #reset');
});

});

describe('Mouestrap.addKeycodes', function() {
Expand Down