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(">+~`!@$%^&={}\\;/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; +}; +