Skip to content

Commit 62a9582

Browse files
committed
fix(selector): support multiple :not clauses
Fixes angular#2243
1 parent c8d83db commit 62a9582

File tree

2 files changed

+42
-22
lines changed

2 files changed

+42
-22
lines changed

modules/angular2/src/render/dom/compiler/selector.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ var _SELECTOR_REGEXP = RegExpWrapper.create(
1717
'([-\\w]+)|' + // "tag"
1818
'(?:\\.([-\\w]+))|' + // ".class"
1919
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]" or "[name*=value]"
20-
'(?:\\))|' + // ")"
20+
'(\\))|' + // ")"
2121
'(\\s*,\\s*)'); // ","
2222

2323
/**
@@ -29,11 +29,11 @@ export class CssSelector {
2929
element: string;
3030
classNames: List<string>;
3131
attrs: List<string>;
32-
notSelector: CssSelector;
32+
notSelectors: List<CssSelector>;
3333
static parse(selector: string): List<CssSelector> {
3434
var results = ListWrapper.create();
3535
var _addResult = (res, cssSel) => {
36-
if (isPresent(cssSel.notSelector) && isBlank(cssSel.element) &&
36+
if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) &&
3737
ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) {
3838
cssSel.element = "*";
3939
}
@@ -43,13 +43,15 @@ export class CssSelector {
4343
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
4444
var match;
4545
var current = cssSelector;
46+
var inNot = false;
4647
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
4748
if (isPresent(match[1])) {
48-
if (isPresent(cssSelector.notSelector)) {
49+
if (inNot) {
4950
throw new BaseException('Nesting :not is not allowed in a selector');
5051
}
51-
current.notSelector = new CssSelector();
52-
current = current.notSelector;
52+
inNot = true;
53+
current = new CssSelector();
54+
ListWrapper.push(cssSelector.notSelectors, current);
5355
}
5456
if (isPresent(match[2])) {
5557
current.setElement(match[2]);
@@ -61,6 +63,13 @@ export class CssSelector {
6163
current.addAttribute(match[4], match[5]);
6264
}
6365
if (isPresent(match[6])) {
66+
inNot = false;
67+
current = cssSelector;
68+
}
69+
if (isPresent(match[7])) {
70+
if (inNot) {
71+
throw new BaseException('Multiple selectors in :not are not supported');
72+
}
6473
_addResult(results, cssSelector);
6574
cssSelector = current = new CssSelector();
6675
}
@@ -73,12 +82,12 @@ export class CssSelector {
7382
this.element = null;
7483
this.classNames = ListWrapper.create();
7584
this.attrs = ListWrapper.create();
76-
this.notSelector = null;
85+
this.notSelectors = ListWrapper.create();
7786
}
7887

7988
isElementSelector(): boolean {
8089
return isPresent(this.element) && ListWrapper.isEmpty(this.classNames) &&
81-
ListWrapper.isEmpty(this.attrs) && isBlank(this.notSelector);
90+
ListWrapper.isEmpty(this.attrs) && this.notSelectors.length === 0;
8291
}
8392

8493
setElement(element: string = null) {
@@ -121,9 +130,8 @@ export class CssSelector {
121130
res += ']';
122131
}
123132
}
124-
if (isPresent(this.notSelector)) {
125-
res += ":not(" + this.notSelector.toString() + ")";
126-
}
133+
ListWrapper.forEach(this.notSelectors,
134+
(notSelector) => { res += ":not(" + notSelector.toString() + ")"; });
127135
return res;
128136
}
129137
}
@@ -133,9 +141,9 @@ export class CssSelector {
133141
* are contained in a given CssSelector.
134142
*/
135143
export class SelectorMatcher {
136-
static createNotMatcher(notSelector: CssSelector) {
144+
static createNotMatcher(notSelectors: List<CssSelector>) {
137145
var notMatcher = new SelectorMatcher();
138-
notMatcher._addSelectable(notSelector, null, null);
146+
notMatcher.addSelectables(notSelectors, null);
139147
return notMatcher;
140148
}
141149

@@ -357,22 +365,22 @@ class SelectorListContext {
357365
// Store context to pass back selector and context when a selector is matched
358366
class SelectorContext {
359367
selector: CssSelector;
360-
notSelector: CssSelector;
368+
notSelectors: List<CssSelector>;
361369
cbContext; // callback context
362370
listContext: SelectorListContext;
363371

364372
constructor(selector: CssSelector, cbContext: any, listContext: SelectorListContext) {
365373
this.selector = selector;
366-
this.notSelector = selector.notSelector;
374+
this.notSelectors = selector.notSelectors;
367375
this.cbContext = cbContext;
368376
this.listContext = listContext;
369377
}
370378

371379
finalize(cssSelector: CssSelector, callback /*: (CssSelector, any) => void*/) {
372380
var result = true;
373-
if (isPresent(this.notSelector) &&
381+
if (this.notSelectors.length > 0 &&
374382
(isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
375-
var notMatcher = SelectorMatcher.createNotMatcher(this.notSelector);
383+
var notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
376384
result = !notMatcher.match(cssSelector, null);
377385
}
378386
if (result && isPresent(callback) &&

modules/angular2/test/render/dom/compiler/selector_spec.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ export function main() {
184184
expect(matched).toEqual([s1[0], 1, s2[0], 2, s3[0], 3, s4[0], 4]);
185185
});
186186

187+
it('should match with multiple :not selectors', () => {
188+
matcher.addSelectables(s1 = CssSelector.parse('div:not([a]):not([b])'), 1);
189+
expect(matcher.match(CssSelector.parse('div[a]')[0], selectableCollector)).toBe(false);
190+
expect(matcher.match(CssSelector.parse('div[b]')[0], selectableCollector)).toBe(false);
191+
expect(matcher.match(CssSelector.parse('div[c]')[0], selectableCollector)).toBe(true);
192+
});
193+
187194
it('should select with one match in a list', () => {
188195
matcher.addSelectables(s1 = CssSelector.parse('input[type=text], textbox'), 1);
189196

@@ -256,7 +263,7 @@ export function main() {
256263
expect(cssSelector.attrs.length).toEqual(0);
257264
expect(cssSelector.classNames.length).toEqual(0);
258265

259-
var notSelector = cssSelector.notSelector;
266+
var notSelector = cssSelector.notSelectors[0];
260267
expect(notSelector.element).toEqual(null);
261268
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
262269
expect(notSelector.classNames).toEqual(['someclass']);
@@ -268,7 +275,7 @@ export function main() {
268275
var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)')[0];
269276
expect(cssSelector.element).toEqual("*");
270277

271-
var notSelector = cssSelector.notSelector;
278+
var notSelector = cssSelector.notSelectors[0];
272279
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
273280
expect(notSelector.classNames).toEqual(['someclass']);
274281

@@ -280,6 +287,11 @@ export function main() {
280287
.toThrowError('Nesting :not is not allowed in a selector');
281288
});
282289

290+
it('should throw when multiple selectors in :not', () => {
291+
expect(() => { CssSelector.parse('sometag:not(a,b)'); })
292+
.toThrowError('Multiple selectors in :not are not supported');
293+
});
294+
283295
it('should detect lists of selectors', () => {
284296
var cssSelectors = CssSelector.parse('.someclass,[attrname=attrvalue], sometag');
285297
expect(cssSelectors.length).toEqual(3);
@@ -298,10 +310,10 @@ export function main() {
298310
expect(cssSelectors[0].attrs).toEqual(['type', 'text']);
299311

300312
expect(cssSelectors[1].element).toEqual('*');
301-
expect(cssSelectors[1].notSelector.element).toEqual('textarea');
313+
expect(cssSelectors[1].notSelectors[0].element).toEqual('textarea');
302314

303315
expect(cssSelectors[2].element).toEqual('textbox');
304-
expect(cssSelectors[2].notSelector.classNames).toEqual(['special']);
316+
expect(cssSelectors[2].notSelectors[0].classNames).toEqual(['special']);
305317
});
306318
});
307-
}
319+
}

0 commit comments

Comments
 (0)