Skip to content

Commit 8d2ee6b

Browse files
committed
feat(selector): add support for :not
Fixes angular#609 Closes angular#948
1 parent 5c1c534 commit 8d2ee6b

File tree

2 files changed

+139
-43
lines changed

2 files changed

+139
-43
lines changed

modules/angular2/src/core/compiler/selector.js

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
2-
import {isPresent, isBlank, RegExpWrapper, RegExpMatcherWrapper, StringWrapper} from 'angular2/src/facade/lang';
2+
import {isPresent, isBlank, RegExpWrapper, RegExpMatcherWrapper, StringWrapper, BaseException} from 'angular2/src/facade/lang';
33

44
const _EMPTY_ATTR_VALUE = '';
55

66
// TODO: Can't use `const` here as
77
// in Dart this is not transpiled into `final` yet...
88
var _SELECTOR_REGEXP =
9-
RegExpWrapper.create('^([-\\w]+)|' + // "tag"
9+
RegExpWrapper.create('(\\:not\\()|' + //":not("
10+
'([-\\w]+)|' + // "tag"
1011
'(?:\\.([-\\w]+))|' + // ".class"
1112
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])'); // "[name]", "[name=value]" or "[name*=value]"
1213

@@ -19,28 +20,43 @@ export class CssSelector {
1920
element:string;
2021
classNames:List;
2122
attrs:List;
22-
static parse(selector:string):CssSelector {
23+
notSelector: CssSelector;
24+
static parse(selector:string): CssSelector {
2325
var cssSelector = new CssSelector();
2426
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
2527
var match;
28+
var current = cssSelector;
2629
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
2730
if (isPresent(match[1])) {
28-
cssSelector.setElement(match[1]);
31+
if (isPresent(cssSelector.notSelector)) {
32+
throw new BaseException('Nesting :not is not allowed in a selector');
33+
}
34+
current.notSelector = new CssSelector();
35+
current = current.notSelector;
2936
}
3037
if (isPresent(match[2])) {
31-
cssSelector.addClassName(match[2]);
38+
current.setElement(match[2]);
3239
}
3340
if (isPresent(match[3])) {
34-
cssSelector.addAttribute(match[3], match[4]);
41+
current.addClassName(match[3]);
42+
}
43+
if (isPresent(match[4])) {
44+
current.addAttribute(match[4], match[5]);
3545
}
3646
}
47+
if (isPresent(cssSelector.notSelector) && isBlank(cssSelector.element)
48+
&& ListWrapper.isEmpty(cssSelector.classNames) && ListWrapper.isEmpty(cssSelector.attrs)) {
49+
cssSelector.element = "*";
50+
}
51+
3752
return cssSelector;
3853
}
3954

4055
constructor() {
4156
this.element = null;
4257
this.classNames = ListWrapper.create();
4358
this.attrs = ListWrapper.create();
59+
this.notSelector = null;
4460
}
4561

4662
setElement(element:string = null) {
@@ -85,6 +101,9 @@ export class CssSelector {
85101
res += ']';
86102
}
87103
}
104+
if (isPresent(this.notSelector)) {
105+
res += ":not(" + this.notSelector.toString() + ")";
106+
}
88107
return res;
89108
}
90109
}
@@ -188,20 +207,22 @@ export class SelectorMatcher {
188207
* whose css selector is contained in the given css selector.
189208
* @param cssSelector A css selector
190209
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
210+
* @return boolean true if a match was found
191211
*/
192-
match(cssSelector:CssSelector, matchedCallback:Function) {
212+
match(cssSelector:CssSelector, matchedCallback:Function):boolean {
213+
var result = false;
193214
var element = cssSelector.element;
194215
var classNames = cssSelector.classNames;
195216
var attrs = cssSelector.attrs;
196217

197-
this._matchTerminal(this._elementMap, element, matchedCallback);
198-
this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback);
218+
result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result;
219+
result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) || result;
199220

200221
if (isPresent(classNames)) {
201222
for (var index = 0; index<classNames.length; index++) {
202223
var className = classNames[index];
203-
this._matchTerminal(this._classMap, className, matchedCallback);
204-
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback);
224+
result = this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
225+
result = this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) || result;
205226
}
206227
}
207228

@@ -212,54 +233,77 @@ export class SelectorMatcher {
212233

213234
var valuesMap = MapWrapper.get(this._attrValueMap, attrName);
214235
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
215-
this._matchTerminal(valuesMap, _EMPTY_ATTR_VALUE, matchedCallback);
236+
result = this._matchTerminal(valuesMap, _EMPTY_ATTR_VALUE, cssSelector, matchedCallback) || result;
216237
}
217-
this._matchTerminal(valuesMap, attrValue, matchedCallback);
238+
result = this._matchTerminal(valuesMap, attrValue, cssSelector, matchedCallback) || result;
218239

219240
valuesMap = MapWrapper.get(this._attrValuePartialMap, attrName)
220-
this._matchPartial(valuesMap, attrValue, cssSelector, matchedCallback);
241+
result = this._matchPartial(valuesMap, attrValue, cssSelector, matchedCallback) || result;
221242
}
222243
}
244+
return result;
223245
}
224246

225-
_matchTerminal(map:Map<string,string> = null, name, matchedCallback) {
247+
_matchTerminal(map:Map<string,string> = null, name, cssSelector, matchedCallback):boolean {
226248
if (isBlank(map) || isBlank(name)) {
227-
return;
249+
return false;
250+
}
251+
252+
var selectables = MapWrapper.get(map, name);
253+
var starSelectables = MapWrapper.get(map, "*");
254+
if (isPresent(starSelectables)) {
255+
selectables = ListWrapper.concat(selectables, starSelectables);
228256
}
229-
var selectables = MapWrapper.get(map, name)
230257
if (isBlank(selectables)) {
231-
return;
258+
return false;
232259
}
233260
var selectable;
261+
var result = false;
234262
for (var index=0; index<selectables.length; index++) {
235263
selectable = selectables[index];
236-
matchedCallback(selectable.selector, selectable.cbContext);
264+
result = selectable.finalize(cssSelector, matchedCallback) || result;
237265
}
266+
return result;
238267
}
239268

240-
_matchPartial(map:Map<string,string> = null, name, cssSelector, matchedCallback) {
269+
_matchPartial(map:Map<string,string> = null, name, cssSelector, matchedCallback):boolean {
241270
if (isBlank(map) || isBlank(name)) {
242-
return;
271+
return false;
243272
}
244273
var nestedSelector = MapWrapper.get(map, name)
245274
if (isBlank(nestedSelector)) {
246-
return;
275+
return false;
247276
}
248277
// TODO(perf): get rid of recursion and measure again
249278
// TODO(perf): don't pass the whole selector into the recursion,
250279
// but only the not processed parts
251-
nestedSelector.match(cssSelector, matchedCallback);
280+
return nestedSelector.match(cssSelector, matchedCallback);
252281
}
253282
}
254283

255284

256285
// Store context to pass back selector and context when a selector is matched
257286
class SelectorContext {
258287
selector:CssSelector;
288+
notSelector:CssSelector;
259289
cbContext; // callback context
260290

261291
constructor(selector:CssSelector, cbContext) {
262292
this.selector = selector;
293+
this.notSelector = selector.notSelector;
263294
this.cbContext = cbContext;
264295
}
296+
297+
finalize(cssSelector: CssSelector, callback) {
298+
var result = true;
299+
if (isPresent(this.notSelector)) {
300+
var notMatcher = new SelectorMatcher();
301+
notMatcher.addSelectable(this.notSelector, null);
302+
result = !notMatcher.match(cssSelector, null);
303+
}
304+
if (result && isPresent(callback)) {
305+
callback(this.selector, this.cbContext);
306+
}
307+
return result;
308+
}
265309
}

modules/angular2/test/core/compiler/selector_spec.js

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,44 +25,44 @@ export function main() {
2525
it('should select by element name case insensitive', () => {
2626
matcher.addSelectable(s1 = CssSelector.parse('someTag'), 1);
2727

28-
matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector);
28+
expect(matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector)).toEqual(false);
2929
expect(matched).toEqual([]);
3030

31-
matcher.match(CssSelector.parse('SOMETAG'), selectableCollector);
31+
expect(matcher.match(CssSelector.parse('SOMETAG'), selectableCollector)).toEqual(true);
3232
expect(matched).toEqual([s1,1]);
3333
});
3434

3535
it('should select by class name case insensitive', () => {
3636
matcher.addSelectable(s1 = CssSelector.parse('.someClass'), 1);
3737
matcher.addSelectable(s2 = CssSelector.parse('.someClass.class2'), 2);
3838

39-
matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector);
39+
expect(matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector)).toEqual(false);
4040
expect(matched).toEqual([]);
4141

42-
matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector);
42+
expect(matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector)).toEqual(true);
4343
expect(matched).toEqual([s1,1]);
4444

4545
reset();
46-
matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector);
46+
expect(matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector)).toEqual(true);
4747
expect(matched).toEqual([s1,1,s2,2]);
4848
});
4949

5050
it('should select by attr name case insensitive independent of the value', () => {
5151
matcher.addSelectable(s1 = CssSelector.parse('[someAttr]'), 1);
5252
matcher.addSelectable(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2);
5353

54-
matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector);
54+
expect(matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector)).toEqual(false);
5555
expect(matched).toEqual([]);
5656

57-
matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector);
57+
expect(matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector)).toEqual(true);
5858
expect(matched).toEqual([s1,1]);
5959

6060
reset();
61-
matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector);
61+
expect(matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector)).toEqual(true);
6262
expect(matched).toEqual([s1,1]);
6363

6464
reset();
65-
matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector);
65+
expect(matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector)).toEqual(true);
6666
expect(matched).toEqual([s1,1,s2,2]);
6767
});
6868

@@ -80,29 +80,29 @@ export function main() {
8080
it('should select by attr name and value case insensitive', () => {
8181
matcher.addSelectable(s1 = CssSelector.parse('[someAttr=someValue]'), 1);
8282

83-
matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector);
83+
expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector)).toEqual(false);
8484
expect(matched).toEqual([]);
8585

86-
matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector);
86+
expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector)).toEqual(true);
8787
expect(matched).toEqual([s1,1]);
8888
});
8989

9090
it('should select by element name, class name and attribute name with value', () => {
9191
matcher.addSelectable(s1 = CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1);
9292

93-
matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector);
93+
expect(matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector)).toEqual(false);
9494
expect(matched).toEqual([]);
9595

96-
matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]'), selectableCollector);
96+
expect(matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]'), selectableCollector)).toEqual(false);
9797
expect(matched).toEqual([]);
9898

99-
matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]'), selectableCollector);
99+
expect(matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]'), selectableCollector)).toEqual(false);
100100
expect(matched).toEqual([]);
101101

102-
matcher.match(CssSelector.parse('someTag.someClass[someAttr]'), selectableCollector);
102+
expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr]'), selectableCollector)).toEqual(false);
103103
expect(matched).toEqual([]);
104104

105-
matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector);
105+
expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector)).toEqual(true);
106106
expect(matched).toEqual([s1,1]);
107107
});
108108

@@ -112,21 +112,42 @@ export function main() {
112112
matcher.addSelectable(s3 = CssSelector.parse('.class1.class2'), 3);
113113
matcher.addSelectable(s4 = CssSelector.parse('.class2.class1'), 4);
114114

115-
matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector);
115+
expect(matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector)).toEqual(true);
116116
expect(matched).toEqual([s1,1,s2,2]);
117117

118118
reset();
119-
matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector);
119+
expect(matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector)).toEqual(true);
120120
expect(matched).toEqual([s1,1,s2,2]);
121121

122122
reset();
123-
matcher.match(CssSelector.parse('.class1.class2'), selectableCollector);
123+
expect(matcher.match(CssSelector.parse('.class1.class2'), selectableCollector)).toEqual(true);
124124
expect(matched).toEqual([s3,3,s4,4]);
125125

126126
reset();
127-
matcher.match(CssSelector.parse('.class2.class1'), selectableCollector);
127+
expect(matcher.match(CssSelector.parse('.class2.class1'), selectableCollector)).toEqual(true);
128128
expect(matched).toEqual([s4,4,s3,3]);
129129
});
130+
131+
it('should not select with a matching :not selector', () => {
132+
matcher.addSelectable(CssSelector.parse('p:not(.someClass)'), 1);
133+
matcher.addSelectable(CssSelector.parse('p:not([someAttr])'), 2);
134+
matcher.addSelectable(CssSelector.parse(':not(.someClass)'), 3);
135+
matcher.addSelectable(CssSelector.parse(':not(p)'), 4);
136+
matcher.addSelectable(CssSelector.parse(':not(p[someAttr])'), 5);
137+
138+
expect(matcher.match(CssSelector.parse('p.someClass[someAttr]'), selectableCollector)).toEqual(false);
139+
expect(matched).toEqual([]);
140+
});
141+
142+
it('should select with a non matching :not selector', () => {
143+
matcher.addSelectable(s1 = CssSelector.parse('p:not(.someClass)'), 1);
144+
matcher.addSelectable(s2 = CssSelector.parse('p:not(.someOtherClass[someAttr])'), 2);
145+
matcher.addSelectable(s3 = CssSelector.parse(':not(.someClass)'), 3);
146+
matcher.addSelectable(s4 = CssSelector.parse(':not(.someOtherClass[someAttr])'), 4);
147+
148+
expect(matcher.match(CssSelector.parse('p[someOtherAttr].someOtherClass'), selectableCollector)).toEqual(true);
149+
expect(matched).toEqual([s1,1,s2,2,s3,3,s4,4]);
150+
});
130151
});
131152

132153
describe('CssSelector.parse', () => {
@@ -164,5 +185,36 @@ export function main() {
164185

165186
expect(cssSelector.toString()).toEqual('sometag.someclass[attrname=attrvalue]');
166187
});
188+
189+
it('should detect :not', () => {
190+
var cssSelector = CssSelector.parse('sometag:not([attrname=attrvalue].someclass)');
191+
expect(cssSelector.element).toEqual('sometag');
192+
expect(cssSelector.attrs.length).toEqual(0);
193+
expect(cssSelector.classNames.length).toEqual(0);
194+
195+
var notSelector = cssSelector.notSelector;
196+
expect(notSelector.element).toEqual(null);
197+
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
198+
expect(notSelector.classNames).toEqual(['someclass']);
199+
200+
expect(cssSelector.toString()).toEqual('sometag:not(.someclass[attrname=attrvalue])');
201+
});
202+
203+
it('should detect :not without truthy', () => {
204+
var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)');
205+
expect(cssSelector.element).toEqual("*");
206+
207+
var notSelector = cssSelector.notSelector;
208+
expect(notSelector.attrs).toEqual(['attrname', 'attrvalue']);
209+
expect(notSelector.classNames).toEqual(['someclass']);
210+
211+
expect(cssSelector.toString()).toEqual('*:not(.someclass[attrname=attrvalue])');
212+
});
213+
214+
it('should throw when nested :not', () => {
215+
expect(() => {
216+
CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))')
217+
}).toThrowError('Nesting :not is not allowed in a selector');
218+
});
167219
});
168220
}

0 commit comments

Comments
 (0)