mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 10:17:11 +03:00
Favor key bindings with the most specific CSS selectors
Now if a keypress event bubbles to an element with bindings for multiple matching selectors, the most specific selector is chosen. This allows us to override a keybinding by using a more specific selector. For example, say we have an editor instance inside of the file-finder widget, so that the user can use all their normal bindings when they type the name of the file. Except when they hit <esc> we want to close the file finder, not do whatever they normally have it bound to. Now we can just say: ``` .file-finder .editor { <esc>: close-file-finder } ``` And we're assured that our binding will take precedence.
This commit is contained in:
parent
f5be55e000
commit
65605409b0
@ -7,7 +7,7 @@ describe "KeyEventHandler", ->
|
|||||||
beforeEach ->
|
beforeEach ->
|
||||||
handler = new KeyEventHandler
|
handler = new KeyEventHandler
|
||||||
|
|
||||||
fdescribe "handleKeypress", ->
|
describe "handleKeypress", ->
|
||||||
fragment = null
|
fragment = null
|
||||||
deleteCharHandler = null
|
deleteCharHandler = null
|
||||||
insertCharHandler = null
|
insertCharHandler = null
|
||||||
@ -68,3 +68,22 @@ describe "KeyEventHandler", ->
|
|||||||
expect(deleteCharHandler).not.toHaveBeenCalled()
|
expect(deleteCharHandler).not.toHaveBeenCalled()
|
||||||
expect(insertCharHandler).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()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
$ = require 'jquery'
|
$ = require 'jquery'
|
||||||
|
Specificity = require 'specificity'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class BindingSet
|
class BindingSet
|
||||||
@ -13,6 +14,7 @@ class BindingSet
|
|||||||
bindings: null
|
bindings: null
|
||||||
|
|
||||||
constructor: (@selector, @bindings) ->
|
constructor: (@selector, @bindings) ->
|
||||||
|
@specificity = Specificity(@selector)
|
||||||
|
|
||||||
commandForEvent: (event) ->
|
commandForEvent: (event) ->
|
||||||
for pattern, command of @bindings
|
for pattern, command of @bindings
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
$ = require 'jquery'
|
$ = require 'jquery'
|
||||||
BindingSet = require 'binding-set'
|
BindingSet = require 'binding-set'
|
||||||
|
Specificity = require 'specificity'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class KeyEventHandler
|
class KeyEventHandler
|
||||||
@ -12,12 +13,13 @@ class KeyEventHandler
|
|||||||
@bindingSets.push(new BindingSet(selector, bindings))
|
@bindingSets.push(new BindingSet(selector, bindings))
|
||||||
|
|
||||||
handleKeypress: (event) ->
|
handleKeypress: (event) ->
|
||||||
currentNode = event.target
|
currentNode = $(event.target)
|
||||||
while currentNode
|
while currentNode
|
||||||
for bindingSet in @bindingSets
|
candidateBindingSets = @bindingSets.filter (set) -> currentNode.is(set.selector)
|
||||||
if $(currentNode).is(bindingSet.selector)
|
candidateBindingSets.sort (a, b) -> b.specificity - a.specificity
|
||||||
if command = bindingSet.commandForEvent(event)
|
for bindingSet in candidateBindingSets
|
||||||
$(event.target).trigger(command)
|
if command = bindingSet.commandForEvent(event)
|
||||||
return
|
$(event.target).trigger(command)
|
||||||
currentNode = currentNode.parentNode
|
return
|
||||||
|
currentNode = currentNode.parent()
|
||||||
|
|
||||||
|
232
vendor/slick.js
vendored
Normal file
232
vendor/slick.js
vendored
Normal file
@ -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 <http://stevenlevithan.com/regex/xregexp/> 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* ( <combinator>+ ) \\s* # Combinator \n\
|
||||||
|
| ( \\s+ ) # CombinatorChildren \n\
|
||||||
|
| ( <unicode>+ | \\* ) # Tag \n\
|
||||||
|
| \\# ( <unicode>+ ) # ID \n\
|
||||||
|
| \\. ( <unicode>+ ) # ClassName \n\
|
||||||
|
| # Attribute \n\
|
||||||
|
\\[ \
|
||||||
|
\\s* (<unicode1>+) (?: \
|
||||||
|
\\s* ([*^$!~|]?=) (?: \
|
||||||
|
\\s* (?:\
|
||||||
|
([\"']?)(.*?)\\9 \
|
||||||
|
)\
|
||||||
|
) \
|
||||||
|
)? \\s* \
|
||||||
|
\\](?!\\]) \n\
|
||||||
|
| :+ ( <unicode>+ )(?:\
|
||||||
|
\\( (?:\
|
||||||
|
(?:([\"'])([^\\12]*)\\12)|((?:\\([^)]+\\)|[^()]*)+)\
|
||||||
|
) \\)\
|
||||||
|
)?\
|
||||||
|
)"
|
||||||
|
*/
|
||||||
|
"^(?:\\s*(,)\\s*|\\s*(<combinator>+)\\s*|(\\s+)|(<unicode>+|\\*)|\\#(<unicode>+)|\\.(<unicode>+)|\\[\\s*(<unicode1>+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(<unicode>+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)"
|
||||||
|
.replace(/<combinator>/, '[' + escapeRegExp(">+~`!@$%^&={}\\;</") + ']')
|
||||||
|
.replace(/<unicode>/g, '(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])')
|
||||||
|
.replace(/<unicode1>/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);
|
33
vendor/specificity.js
vendored
Normal file
33
vendor/specificity.js
vendored
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user