From 62a95823e0ebef984a99db37082b2da3ff50d9b4 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Mon, 1 Jun 2015 14:24:19 -0700 Subject: [PATCH] fix(selector): support multiple `:not` clauses Fixes #2243 --- .../src/render/dom/compiler/selector.ts | 42 +++++++++++-------- .../test/render/dom/compiler/selector_spec.ts | 22 +++++++--- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/modules/angular2/src/render/dom/compiler/selector.ts b/modules/angular2/src/render/dom/compiler/selector.ts index 906829fd9d..54d4f99660 100644 --- a/modules/angular2/src/render/dom/compiler/selector.ts +++ b/modules/angular2/src/render/dom/compiler/selector.ts @@ -17,7 +17,7 @@ var _SELECTOR_REGEXP = RegExpWrapper.create( '([-\\w]+)|' + // "tag" '(?:\\.([-\\w]+))|' + // ".class" '(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]" or "[name*=value]" - '(?:\\))|' + // ")" + '(\\))|' + // ")" '(\\s*,\\s*)'); // "," /** @@ -29,11 +29,11 @@ export class CssSelector { element: string; classNames: List; attrs: List; - notSelector: CssSelector; + notSelectors: List; static parse(selector: string): List { var results = ListWrapper.create(); var _addResult = (res, cssSel) => { - if (isPresent(cssSel.notSelector) && isBlank(cssSel.element) && + if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) && ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) { cssSel.element = "*"; } @@ -43,13 +43,15 @@ export class CssSelector { var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector); var match; var current = cssSelector; + var inNot = false; while (isPresent(match = RegExpMatcherWrapper.next(matcher))) { if (isPresent(match[1])) { - if (isPresent(cssSelector.notSelector)) { + if (inNot) { throw new BaseException('Nesting :not is not allowed in a selector'); } - current.notSelector = new CssSelector(); - current = current.notSelector; + inNot = true; + current = new CssSelector(); + ListWrapper.push(cssSelector.notSelectors, current); } if (isPresent(match[2])) { current.setElement(match[2]); @@ -61,6 +63,13 @@ export class CssSelector { current.addAttribute(match[4], match[5]); } if (isPresent(match[6])) { + inNot = false; + current = cssSelector; + } + if (isPresent(match[7])) { + if (inNot) { + throw new BaseException('Multiple selectors in :not are not supported'); + } _addResult(results, cssSelector); cssSelector = current = new CssSelector(); } @@ -73,12 +82,12 @@ export class CssSelector { this.element = null; this.classNames = ListWrapper.create(); this.attrs = ListWrapper.create(); - this.notSelector = null; + this.notSelectors = ListWrapper.create(); } isElementSelector(): boolean { return isPresent(this.element) && ListWrapper.isEmpty(this.classNames) && - ListWrapper.isEmpty(this.attrs) && isBlank(this.notSelector); + ListWrapper.isEmpty(this.attrs) && this.notSelectors.length === 0; } setElement(element: string = null) { @@ -121,9 +130,8 @@ export class CssSelector { res += ']'; } } - if (isPresent(this.notSelector)) { - res += ":not(" + this.notSelector.toString() + ")"; - } + ListWrapper.forEach(this.notSelectors, + (notSelector) => { res += ":not(" + notSelector.toString() + ")"; }); return res; } } @@ -133,9 +141,9 @@ export class CssSelector { * are contained in a given CssSelector. */ export class SelectorMatcher { - static createNotMatcher(notSelector: CssSelector) { + static createNotMatcher(notSelectors: List) { var notMatcher = new SelectorMatcher(); - notMatcher._addSelectable(notSelector, null, null); + notMatcher.addSelectables(notSelectors, null); return notMatcher; } @@ -357,22 +365,22 @@ class SelectorListContext { // Store context to pass back selector and context when a selector is matched class SelectorContext { selector: CssSelector; - notSelector: CssSelector; + notSelectors: List; cbContext; // callback context listContext: SelectorListContext; constructor(selector: CssSelector, cbContext: any, listContext: SelectorListContext) { this.selector = selector; - this.notSelector = selector.notSelector; + this.notSelectors = selector.notSelectors; this.cbContext = cbContext; this.listContext = listContext; } finalize(cssSelector: CssSelector, callback /*: (CssSelector, any) => void*/) { var result = true; - if (isPresent(this.notSelector) && + if (this.notSelectors.length > 0 && (isBlank(this.listContext) || !this.listContext.alreadyMatched)) { - var notMatcher = SelectorMatcher.createNotMatcher(this.notSelector); + var notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors); result = !notMatcher.match(cssSelector, null); } if (result && isPresent(callback) && diff --git a/modules/angular2/test/render/dom/compiler/selector_spec.ts b/modules/angular2/test/render/dom/compiler/selector_spec.ts index 58e415c4ac..28ddd24d1c 100644 --- a/modules/angular2/test/render/dom/compiler/selector_spec.ts +++ b/modules/angular2/test/render/dom/compiler/selector_spec.ts @@ -184,6 +184,13 @@ export function main() { expect(matched).toEqual([s1[0], 1, s2[0], 2, s3[0], 3, s4[0], 4]); }); + it('should match with multiple :not selectors', () => { + matcher.addSelectables(s1 = CssSelector.parse('div:not([a]):not([b])'), 1); + expect(matcher.match(CssSelector.parse('div[a]')[0], selectableCollector)).toBe(false); + expect(matcher.match(CssSelector.parse('div[b]')[0], selectableCollector)).toBe(false); + expect(matcher.match(CssSelector.parse('div[c]')[0], selectableCollector)).toBe(true); + }); + it('should select with one match in a list', () => { matcher.addSelectables(s1 = CssSelector.parse('input[type=text], textbox'), 1); @@ -256,7 +263,7 @@ export function main() { expect(cssSelector.attrs.length).toEqual(0); expect(cssSelector.classNames.length).toEqual(0); - var notSelector = cssSelector.notSelector; + var notSelector = cssSelector.notSelectors[0]; expect(notSelector.element).toEqual(null); expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']); expect(notSelector.classNames).toEqual(['someclass']); @@ -268,7 +275,7 @@ export function main() { var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)')[0]; expect(cssSelector.element).toEqual("*"); - var notSelector = cssSelector.notSelector; + var notSelector = cssSelector.notSelectors[0]; expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']); expect(notSelector.classNames).toEqual(['someclass']); @@ -280,6 +287,11 @@ export function main() { .toThrowError('Nesting :not is not allowed in a selector'); }); + it('should throw when multiple selectors in :not', () => { + expect(() => { CssSelector.parse('sometag:not(a,b)'); }) + .toThrowError('Multiple selectors in :not are not supported'); + }); + it('should detect lists of selectors', () => { var cssSelectors = CssSelector.parse('.someclass,[attrname=attrvalue], sometag'); expect(cssSelectors.length).toEqual(3); @@ -298,10 +310,10 @@ export function main() { expect(cssSelectors[0].attrs).toEqual(['type', 'text']); expect(cssSelectors[1].element).toEqual('*'); - expect(cssSelectors[1].notSelector.element).toEqual('textarea'); + expect(cssSelectors[1].notSelectors[0].element).toEqual('textarea'); expect(cssSelectors[2].element).toEqual('textbox'); - expect(cssSelectors[2].notSelector.classNames).toEqual(['special']); + expect(cssSelectors[2].notSelectors[0].classNames).toEqual(['special']); }); }); -} \ No newline at end of file +}