diff --git a/spec/atom/key-event-handler-spec.coffee b/spec/atom/key-event-handler-spec.coffee
index b09223dae..2744aa22c 100644
--- a/spec/atom/key-event-handler-spec.coffee
+++ b/spec/atom/key-event-handler-spec.coffee
@@ -7,7 +7,7 @@ describe "KeyEventHandler", ->
beforeEach ->
handler = new KeyEventHandler
- fdescribe "handleKeypress", ->
+ describe "handleKeypress", ->
fragment = null
deleteCharHandler = null
insertCharHandler = null
@@ -68,3 +68,22 @@ describe "KeyEventHandler", ->
expect(deleteCharHandler).not.toHaveBeenCalled()
expect(insertCharHandler).not.toHaveBeenCalled()
+ describe "when the event bubbles to a node that matches multiple selectors", ->
+ it "triggers the binding for the most specific selector", ->
+ handler.bindKeys 'div .child-node', 'x': 'foo'
+ handler.bindKeys '.command-mode .child-node', 'x': 'baz'
+ handler.bindKeys '.child-node', 'x': 'bar'
+
+ fooHandler = jasmine.createSpy 'fooHandler'
+ barHandler = jasmine.createSpy 'barHandler'
+ bazHandler = jasmine.createSpy 'bazHandler'
+ fragment.on 'foo', fooHandler
+ fragment.on 'bar', barHandler
+ fragment.on 'baz', bazHandler
+
+ target = fragment.find('.grandchild-node')[0]
+ handler.handleKeypress(keypressEvent('x', target: target))
+
+ expect(fooHandler).not.toHaveBeenCalled()
+ expect(barHandler).not.toHaveBeenCalled()
+ expect(bazHandler).toHaveBeenCalled()
diff --git a/src/atom/binding-set.coffee b/src/atom/binding-set.coffee
index 08584506a..39d331dcc 100644
--- a/src/atom/binding-set.coffee
+++ b/src/atom/binding-set.coffee
@@ -1,4 +1,5 @@
$ = require 'jquery'
+Specificity = require 'specificity'
module.exports =
class BindingSet
@@ -13,6 +14,7 @@ class BindingSet
bindings: null
constructor: (@selector, @bindings) ->
+ @specificity = Specificity(@selector)
commandForEvent: (event) ->
for pattern, command of @bindings
diff --git a/src/atom/key-event-handler.coffee b/src/atom/key-event-handler.coffee
index 786cf26b0..132c64e8b 100644
--- a/src/atom/key-event-handler.coffee
+++ b/src/atom/key-event-handler.coffee
@@ -1,5 +1,6 @@
$ = require 'jquery'
BindingSet = require 'binding-set'
+Specificity = require 'specificity'
module.exports =
class KeyEventHandler
@@ -12,12 +13,13 @@ class KeyEventHandler
@bindingSets.push(new BindingSet(selector, bindings))
handleKeypress: (event) ->
- currentNode = event.target
+ currentNode = $(event.target)
while currentNode
- for bindingSet in @bindingSets
- if $(currentNode).is(bindingSet.selector)
- if command = bindingSet.commandForEvent(event)
- $(event.target).trigger(command)
- return
- currentNode = currentNode.parentNode
+ candidateBindingSets = @bindingSets.filter (set) -> currentNode.is(set.selector)
+ candidateBindingSets.sort (a, b) -> b.specificity - a.specificity
+ for bindingSet in candidateBindingSets
+ if command = bindingSet.commandForEvent(event)
+ $(event.target).trigger(command)
+ return
+ currentNode = currentNode.parent()
diff --git a/vendor/slick.js b/vendor/slick.js
new file mode 100644
index 000000000..cbeecdbe4
--- /dev/null
+++ b/vendor/slick.js
@@ -0,0 +1,232 @@
+// changed at the bottom to export the Slick object
+
+/*
+---
+name: Slick.Parser
+description: Standalone CSS3 Selector parser
+provides: Slick.Parser
+...
+*/
+
+;(function(){
+
+var parsed,
+ separatorIndex,
+ combinatorIndex,
+ reversed,
+ cache = {},
+ reverseCache = {},
+ reUnescape = /\\/g;
+
+var parse = function(expression, isReversed){
+ if (expression == null) return null;
+ if (expression.Slick === true) return expression;
+ expression = ('' + expression).replace(/^\s+|\s+$/g, '');
+ reversed = !!isReversed;
+ var currentCache = (reversed) ? reverseCache : cache;
+ if (currentCache[expression]) return currentCache[expression];
+ parsed = {
+ Slick: true,
+ expressions: [],
+ raw: expression,
+ reverse: function(){
+ return parse(this.raw, true);
+ }
+ };
+ separatorIndex = -1;
+ while (expression != (expression = expression.replace(regexp, parser)));
+ parsed.length = parsed.expressions.length;
+ return currentCache[parsed.raw] = (reversed) ? reverse(parsed) : parsed;
+};
+
+var reverseCombinator = function(combinator){
+ if (combinator === '!') return ' ';
+ else if (combinator === ' ') return '!';
+ else if ((/^!/).test(combinator)) return combinator.replace(/^!/, '');
+ else return '!' + combinator;
+};
+
+var reverse = function(expression){
+ var expressions = expression.expressions;
+ for (var i = 0; i < expressions.length; i++){
+ var exp = expressions[i];
+ var last = {parts: [], tag: '*', combinator: reverseCombinator(exp[0].combinator)};
+
+ for (var j = 0; j < exp.length; j++){
+ var cexp = exp[j];
+ if (!cexp.reverseCombinator) cexp.reverseCombinator = ' ';
+ cexp.combinator = cexp.reverseCombinator;
+ delete cexp.reverseCombinator;
+ }
+
+ exp.reverse().push(last);
+ }
+ return expression;
+};
+
+var escapeRegExp = function(string){// Credit: XRegExp 0.6.1 (c) 2007-2008 Steven Levithan MIT License
+ return string.replace(/[-[\]{}()*+?.\\^$|,#\s]/g, function(match){
+ return '\\' + match;
+ });
+};
+
+var regexp = new RegExp(
+/*
+#!/usr/bin/env ruby
+puts "\t\t" + DATA.read.gsub(/\(\?x\)|\s+#.*$|\s+|\\$|\\n/,'')
+__END__
+ "(?x)^(?:\
+ \\s* ( , ) \\s* # Separator \n\
+ | \\s* ( + ) \\s* # Combinator \n\
+ | ( \\s+ ) # CombinatorChildren \n\
+ | ( + | \\* ) # Tag \n\
+ | \\# ( + ) # ID \n\
+ | \\. ( + ) # ClassName \n\
+ | # Attribute \n\
+ \\[ \
+ \\s* (+) (?: \
+ \\s* ([*^$!~|]?=) (?: \
+ \\s* (?:\
+ ([\"']?)(.*?)\\9 \
+ )\
+ ) \
+ )? \\s* \
+ \\](?!\\]) \n\
+ | :+ ( + )(?:\
+ \\( (?:\
+ (?:([\"'])([^\\12]*)\\12)|((?:\\([^)]+\\)|[^()]*)+)\
+ ) \\)\
+ )?\
+ )"
+*/
+ "^(?:\\s*(,)\\s*|\\s*(+)\\s*|(\\s+)|(+|\\*)|\\#(+)|\\.(+)|\\[\\s*(+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)"
+ .replace(//, '[' + escapeRegExp(">+~`!@$%^&={}\\;") + ']')
+ .replace(//g, '(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])')
+ .replace(//g, '(?:[:\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])')
+);
+
+function parser(
+ rawMatch,
+
+ separator,
+ combinator,
+ combinatorChildren,
+
+ tagName,
+ id,
+ className,
+
+ attributeKey,
+ attributeOperator,
+ attributeQuote,
+ attributeValue,
+
+ pseudoMarker,
+ pseudoClass,
+ pseudoQuote,
+ pseudoClassQuotedValue,
+ pseudoClassValue
+){
+ if (separator || separatorIndex === -1){
+ parsed.expressions[++separatorIndex] = [];
+ combinatorIndex = -1;
+ if (separator) return '';
+ }
+
+ if (combinator || combinatorChildren || combinatorIndex === -1){
+ combinator = combinator || ' ';
+ var currentSeparator = parsed.expressions[separatorIndex];
+ if (reversed && currentSeparator[combinatorIndex])
+ currentSeparator[combinatorIndex].reverseCombinator = reverseCombinator(combinator);
+ currentSeparator[++combinatorIndex] = {combinator: combinator, tag: '*'};
+ }
+
+ var currentParsed = parsed.expressions[separatorIndex][combinatorIndex];
+
+ if (tagName){
+ currentParsed.tag = tagName.replace(reUnescape, '');
+
+ } else if (id){
+ currentParsed.id = id.replace(reUnescape, '');
+
+ } else if (className){
+ className = className.replace(reUnescape, '');
+
+ if (!currentParsed.classList) currentParsed.classList = [];
+ if (!currentParsed.classes) currentParsed.classes = [];
+ currentParsed.classList.push(className);
+ currentParsed.classes.push({
+ value: className,
+ regexp: new RegExp('(^|\\s)' + escapeRegExp(className) + '(\\s|$)')
+ });
+
+ } else if (pseudoClass){
+ pseudoClassValue = pseudoClassValue || pseudoClassQuotedValue;
+ pseudoClassValue = pseudoClassValue ? pseudoClassValue.replace(reUnescape, '') : null;
+
+ if (!currentParsed.pseudos) currentParsed.pseudos = [];
+ currentParsed.pseudos.push({
+ key: pseudoClass.replace(reUnescape, ''),
+ value: pseudoClassValue,
+ type: pseudoMarker.length == 1 ? 'class' : 'element'
+ });
+
+ } else if (attributeKey){
+ attributeKey = attributeKey.replace(reUnescape, '');
+ attributeValue = (attributeValue || '').replace(reUnescape, '');
+
+ var test, regexp;
+
+ switch (attributeOperator){
+ case '^=' : regexp = new RegExp( '^'+ escapeRegExp(attributeValue) ); break;
+ case '$=' : regexp = new RegExp( escapeRegExp(attributeValue) +'$' ); break;
+ case '~=' : regexp = new RegExp( '(^|\\s)'+ escapeRegExp(attributeValue) +'(\\s|$)' ); break;
+ case '|=' : regexp = new RegExp( '^'+ escapeRegExp(attributeValue) +'(-|$)' ); break;
+ case '=' : test = function(value){
+ return attributeValue == value;
+ }; break;
+ case '*=' : test = function(value){
+ return value && value.indexOf(attributeValue) > -1;
+ }; break;
+ case '!=' : test = function(value){
+ return attributeValue != value;
+ }; break;
+ default : test = function(value){
+ return !!value;
+ };
+ }
+
+ if (attributeValue == '' && (/^[*$^]=$/).test(attributeOperator)) test = function(){
+ return false;
+ };
+
+ if (!test) test = function(value){
+ return value && regexp.test(value);
+ };
+
+ if (!currentParsed.attributes) currentParsed.attributes = [];
+ currentParsed.attributes.push({
+ key: attributeKey,
+ operator: attributeOperator,
+ value: attributeValue,
+ test: test
+ });
+
+ }
+
+ return '';
+};
+
+// Slick NS
+
+var Slick = (this.Slick || {});
+
+Slick.parse = function(expression){
+ return parse(expression);
+};
+
+Slick.escapeRegExp = escapeRegExp;
+
+this.exports = Slick
+
+}).apply(module);
diff --git a/vendor/specificity.js b/vendor/specificity.js
new file mode 100644
index 000000000..6059a4a26
--- /dev/null
+++ b/vendor/specificity.js
@@ -0,0 +1,33 @@
+// source: MooTools DOM branch -> https://raw.github.com/arian/DOM/matcher-specificity/Source/specificity.js
+// changed to be compatible with our require system
+
+var Slick = require('slick');
+
+module.exports = function(selector){
+
+ var parsed = Slick.parse(selector);
+ var expressions = parsed.expressions;
+ var specificity = -1;
+ for (var j = 0; j < expressions.length; j++){
+ var b = 0, c = 0, d = 0, s = 0, nots = [];
+ for (var i = 0; i < expressions[j].length; i++){
+ var expression = expressions[j][i], pseudos = expression.pseudos;
+ if (expression.id) b++;
+ if (expression.attributes) c += expression.attributes.length;
+ if (expression.classes) c += expression.classes.length;
+ if (expression.tag && expression.tag != '*') d++;
+ if (pseudos){
+ d += pseudos.length;
+ for (var p = 0; p < pseudos.length; p++) if (pseudos[p].key == 'not'){
+ nots.push(pseudos[p].value);
+ d--;
+ }
+ }
+ }
+ s = b * 1e6 + c * 1e3 + d;
+ for (var ii = nots.length; ii--;) s += this.specificity(nots[ii]);
+ if (s > specificity) specificity = s;
+ }
+ return specificity;
+};
+