diff --git a/modules/angular2/src/core/compiler/selector.js b/modules/angular2/src/core/compiler/selector.js index ab86faf228..c0ce8132d6 100644 --- a/modules/angular2/src/core/compiler/selector.js +++ b/modules/angular2/src/core/compiler/selector.js @@ -1,12 +1,13 @@ import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; -import {isPresent, isBlank, RegExpWrapper, RegExpMatcherWrapper, StringWrapper} from 'angular2/src/facade/lang'; +import {isPresent, isBlank, RegExpWrapper, RegExpMatcherWrapper, StringWrapper, BaseException} from 'angular2/src/facade/lang'; const _EMPTY_ATTR_VALUE = ''; // TODO: Can't use `const` here as // in Dart this is not transpiled into `final` yet... var _SELECTOR_REGEXP = - RegExpWrapper.create('^([-\\w]+)|' + // "tag" + RegExpWrapper.create('(\\:not\\()|' + //":not(" + '([-\\w]+)|' + // "tag" '(?:\\.([-\\w]+))|' + // ".class" '(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])'); // "[name]", "[name=value]" or "[name*=value]" @@ -19,21 +20,35 @@ export class CssSelector { element:string; classNames:List; attrs:List; - static parse(selector:string):CssSelector { + notSelector: CssSelector; + static parse(selector:string): CssSelector { var cssSelector = new CssSelector(); var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector); var match; + var current = cssSelector; while (isPresent(match = RegExpMatcherWrapper.next(matcher))) { if (isPresent(match[1])) { - cssSelector.setElement(match[1]); + if (isPresent(cssSelector.notSelector)) { + throw new BaseException('Nesting :not is not allowed in a selector'); + } + current.notSelector = new CssSelector(); + current = current.notSelector; } if (isPresent(match[2])) { - cssSelector.addClassName(match[2]); + current.setElement(match[2]); } if (isPresent(match[3])) { - cssSelector.addAttribute(match[3], match[4]); + current.addClassName(match[3]); + } + if (isPresent(match[4])) { + current.addAttribute(match[4], match[5]); } } + if (isPresent(cssSelector.notSelector) && isBlank(cssSelector.element) + && ListWrapper.isEmpty(cssSelector.classNames) && ListWrapper.isEmpty(cssSelector.attrs)) { + cssSelector.element = "*"; + } + return cssSelector; } @@ -41,6 +56,7 @@ export class CssSelector { this.element = null; this.classNames = ListWrapper.create(); this.attrs = ListWrapper.create(); + this.notSelector = null; } setElement(element:string = null) { @@ -85,6 +101,9 @@ export class CssSelector { res += ']'; } } + if (isPresent(this.notSelector)) { + res += ":not(" + this.notSelector.toString() + ")"; + } return res; } } @@ -188,20 +207,22 @@ export class SelectorMatcher { * whose css selector is contained in the given css selector. * @param cssSelector A css selector * @param matchedCallback This callback will be called with the object handed into `addSelectable` + * @return boolean true if a match was found */ - match(cssSelector:CssSelector, matchedCallback:Function) { + match(cssSelector:CssSelector, matchedCallback:Function):boolean { + var result = false; var element = cssSelector.element; var classNames = cssSelector.classNames; var attrs = cssSelector.attrs; - this._matchTerminal(this._elementMap, element, matchedCallback); - this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback); + result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result; + result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) || result; if (isPresent(classNames)) { for (var index = 0; index = null, name, matchedCallback) { + _matchTerminal(map:Map = null, name, cssSelector, matchedCallback):boolean { if (isBlank(map) || isBlank(name)) { - return; + return false; + } + + var selectables = MapWrapper.get(map, name); + var starSelectables = MapWrapper.get(map, "*"); + if (isPresent(starSelectables)) { + selectables = ListWrapper.concat(selectables, starSelectables); } - var selectables = MapWrapper.get(map, name) if (isBlank(selectables)) { - return; + return false; } var selectable; + var result = false; for (var index=0; index = null, name, cssSelector, matchedCallback) { + _matchPartial(map:Map = null, name, cssSelector, matchedCallback):boolean { if (isBlank(map) || isBlank(name)) { - return; + return false; } var nestedSelector = MapWrapper.get(map, name) if (isBlank(nestedSelector)) { - return; + return false; } // TODO(perf): get rid of recursion and measure again // TODO(perf): don't pass the whole selector into the recursion, // but only the not processed parts - nestedSelector.match(cssSelector, matchedCallback); + return nestedSelector.match(cssSelector, matchedCallback); } } @@ -256,10 +285,25 @@ export class SelectorMatcher { // Store context to pass back selector and context when a selector is matched class SelectorContext { selector:CssSelector; + notSelector:CssSelector; cbContext; // callback context constructor(selector:CssSelector, cbContext) { this.selector = selector; + this.notSelector = selector.notSelector; this.cbContext = cbContext; } + + finalize(cssSelector: CssSelector, callback) { + var result = true; + if (isPresent(this.notSelector)) { + var notMatcher = new SelectorMatcher(); + notMatcher.addSelectable(this.notSelector, null); + result = !notMatcher.match(cssSelector, null); + } + if (result && isPresent(callback)) { + callback(this.selector, this.cbContext); + } + return result; + } } diff --git a/modules/angular2/test/core/compiler/selector_spec.js b/modules/angular2/test/core/compiler/selector_spec.js index 6ef5cf2725..34db287d5f 100644 --- a/modules/angular2/test/core/compiler/selector_spec.js +++ b/modules/angular2/test/core/compiler/selector_spec.js @@ -25,10 +25,10 @@ export function main() { it('should select by element name case insensitive', () => { matcher.addSelectable(s1 = CssSelector.parse('someTag'), 1); - matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector); + expect(matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector)).toEqual(false); expect(matched).toEqual([]); - matcher.match(CssSelector.parse('SOMETAG'), selectableCollector); + expect(matcher.match(CssSelector.parse('SOMETAG'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1]); }); @@ -36,14 +36,14 @@ export function main() { matcher.addSelectable(s1 = CssSelector.parse('.someClass'), 1); matcher.addSelectable(s2 = CssSelector.parse('.someClass.class2'), 2); - matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector); + expect(matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector)).toEqual(false); expect(matched).toEqual([]); - matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector); + expect(matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1]); reset(); - matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector); + expect(matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1,s2,2]); }); @@ -51,18 +51,18 @@ export function main() { matcher.addSelectable(s1 = CssSelector.parse('[someAttr]'), 1); matcher.addSelectable(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2); - matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector); + expect(matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector)).toEqual(false); expect(matched).toEqual([]); - matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector); + expect(matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1]); reset(); - matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector); + expect(matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1]); reset(); - matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector); + expect(matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1,s2,2]); }); @@ -80,29 +80,29 @@ export function main() { it('should select by attr name and value case insensitive', () => { matcher.addSelectable(s1 = CssSelector.parse('[someAttr=someValue]'), 1); - matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector); + expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector)).toEqual(false); expect(matched).toEqual([]); - matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector); + expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1]); }); it('should select by element name, class name and attribute name with value', () => { matcher.addSelectable(s1 = CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1); - matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector); + expect(matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector)).toEqual(false); expect(matched).toEqual([]); - matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]'), selectableCollector); + expect(matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]'), selectableCollector)).toEqual(false); expect(matched).toEqual([]); - matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]'), selectableCollector); + expect(matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]'), selectableCollector)).toEqual(false); expect(matched).toEqual([]); - matcher.match(CssSelector.parse('someTag.someClass[someAttr]'), selectableCollector); + expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr]'), selectableCollector)).toEqual(false); expect(matched).toEqual([]); - matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector); + expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1]); }); @@ -112,21 +112,42 @@ export function main() { matcher.addSelectable(s3 = CssSelector.parse('.class1.class2'), 3); matcher.addSelectable(s4 = CssSelector.parse('.class2.class1'), 4); - matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector); + expect(matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1,s2,2]); reset(); - matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector); + expect(matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector)).toEqual(true); expect(matched).toEqual([s1,1,s2,2]); reset(); - matcher.match(CssSelector.parse('.class1.class2'), selectableCollector); + expect(matcher.match(CssSelector.parse('.class1.class2'), selectableCollector)).toEqual(true); expect(matched).toEqual([s3,3,s4,4]); reset(); - matcher.match(CssSelector.parse('.class2.class1'), selectableCollector); + expect(matcher.match(CssSelector.parse('.class2.class1'), selectableCollector)).toEqual(true); expect(matched).toEqual([s4,4,s3,3]); }); + + it('should not select with a matching :not selector', () => { + matcher.addSelectable(CssSelector.parse('p:not(.someClass)'), 1); + matcher.addSelectable(CssSelector.parse('p:not([someAttr])'), 2); + matcher.addSelectable(CssSelector.parse(':not(.someClass)'), 3); + matcher.addSelectable(CssSelector.parse(':not(p)'), 4); + matcher.addSelectable(CssSelector.parse(':not(p[someAttr])'), 5); + + expect(matcher.match(CssSelector.parse('p.someClass[someAttr]'), selectableCollector)).toEqual(false); + expect(matched).toEqual([]); + }); + + it('should select with a non matching :not selector', () => { + matcher.addSelectable(s1 = CssSelector.parse('p:not(.someClass)'), 1); + matcher.addSelectable(s2 = CssSelector.parse('p:not(.someOtherClass[someAttr])'), 2); + matcher.addSelectable(s3 = CssSelector.parse(':not(.someClass)'), 3); + matcher.addSelectable(s4 = CssSelector.parse(':not(.someOtherClass[someAttr])'), 4); + + expect(matcher.match(CssSelector.parse('p[someOtherAttr].someOtherClass'), selectableCollector)).toEqual(true); + expect(matched).toEqual([s1,1,s2,2,s3,3,s4,4]); + }); }); describe('CssSelector.parse', () => { @@ -164,5 +185,36 @@ export function main() { expect(cssSelector.toString()).toEqual('sometag.someclass[attrname=attrvalue]'); }); + + it('should detect :not', () => { + var cssSelector = CssSelector.parse('sometag:not([attrname=attrvalue].someclass)'); + expect(cssSelector.element).toEqual('sometag'); + expect(cssSelector.attrs.length).toEqual(0); + expect(cssSelector.classNames.length).toEqual(0); + + var notSelector = cssSelector.notSelector; + expect(notSelector.element).toEqual(null); + expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']); + expect(notSelector.classNames).toEqual(['someclass']); + + expect(cssSelector.toString()).toEqual('sometag:not(.someclass[attrname=attrvalue])'); + }); + + it('should detect :not without truthy', () => { + var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)'); + expect(cssSelector.element).toEqual("*"); + + var notSelector = cssSelector.notSelector; + expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']); + expect(notSelector.classNames).toEqual(['someclass']); + + expect(cssSelector.toString()).toEqual('*:not(.someclass[attrname=attrvalue])'); + }); + + it('should throw when nested :not', () => { + expect(() => { + CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))') + }).toThrowError('Nesting :not is not allowed in a selector'); + }); }); } \ No newline at end of file