From 41b53e71e15836ca8a92dd2afee90c01a1832ea5 Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Thu, 19 Mar 2015 17:01:42 +0100 Subject: [PATCH] feat(selector): support , for multiple targets Fixes #867 Closes #1019 --- .../src/core/annotations/annotations.js | 2 +- .../compiler/pipeline/directive_parser.js | 4 +- .../angular2/src/core/compiler/selector.js | 71 ++++++-- modules/angular2/src/dom/parse5_adapter.cjs | 4 +- .../test/core/compiler/selector_spec.js | 172 +++++++++++------- .../src/compiler/selector_benchmark.js | 6 +- 6 files changed, 172 insertions(+), 87 deletions(-) diff --git a/modules/angular2/src/core/annotations/annotations.js b/modules/angular2/src/core/annotations/annotations.js index 4ca676388f..8b8ce8c65a 100644 --- a/modules/angular2/src/core/annotations/annotations.js +++ b/modules/angular2/src/core/annotations/annotations.js @@ -248,7 +248,7 @@ export class Directive extends Injectable { * - `[attribute]`: select by attribute name. * - `[attribute=value]`: select by attribute name and value. * - `:not(sub_selector)`: select only if the element does not match the `sub_selector`. - * - `selector1, selector2`: select if either `selector1` or `selector2` matches. [TO BE IMPLMENTED] + * - `selector1, selector2`: select if either `selector1` or `selector2` matches. * * * ## Example diff --git a/modules/angular2/src/core/compiler/pipeline/directive_parser.js b/modules/angular2/src/core/compiler/pipeline/directive_parser.js index 2c60b0f7f2..34994526d3 100644 --- a/modules/angular2/src/core/compiler/pipeline/directive_parser.js +++ b/modules/angular2/src/core/compiler/pipeline/directive_parser.js @@ -36,8 +36,8 @@ export class DirectiveParser extends CompileStep { this._selectorMatcher = new SelectorMatcher(); for (var i=0; i { + var results = ListWrapper.create(); + var _addResult = (res, cssSel) => { + if (isPresent(cssSel.notSelector) && isBlank(cssSel.element) + && ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) { + cssSel.element = "*"; + } + ListWrapper.push(res, cssSel); + } var cssSelector = new CssSelector(); var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector); var match; @@ -43,13 +53,13 @@ export class CssSelector { if (isPresent(match[4])) { current.addAttribute(match[4], match[5]); } + if (isPresent(match[6])) { + _addResult(results, cssSelector); + cssSelector = current = new CssSelector(); + } } - if (isPresent(cssSelector.notSelector) && isBlank(cssSelector.element) - && ListWrapper.isEmpty(cssSelector.classNames) && ListWrapper.isEmpty(cssSelector.attrs)) { - cssSelector.element = "*"; - } - - return cssSelector; + _addResult(results, cssSelector); + return results; } constructor() { @@ -119,6 +129,7 @@ export class SelectorMatcher { _classPartialMap:Map; _attrValueMap:Map; _attrValuePartialMap:Map; + _listContexts:List; constructor() { this._elementMap = MapWrapper.create(); this._elementPartialMap = MapWrapper.create(); @@ -128,6 +139,19 @@ export class SelectorMatcher { this._attrValueMap = MapWrapper.create(); this._attrValuePartialMap = MapWrapper.create(); + + this._listContexts = ListWrapper.create(); + } + + addSelectables(cssSelectors:List, callbackCtxt) { + var listContext = null; + if (cssSelectors.length > 1) { + listContext= new SelectorListContext(cssSelectors); + ListWrapper.push(this._listContexts, listContext); + } + for (var i = 0; i < cssSelectors.length; i++) { + this.addSelectable(cssSelectors[i], callbackCtxt, listContext); + } } /** @@ -135,12 +159,12 @@ export class SelectorMatcher { * @param cssSelector A css selector * @param callbackCtxt An opaque object that will be given to the callback of the `match` function */ - addSelectable(cssSelector:CssSelector, callbackCtxt) { + addSelectable(cssSelector, callbackCtxt, listContext: SelectorListContext) { var matcher = this; var element = cssSelector.element; var classNames = cssSelector.classNames; var attrs = cssSelector.attrs; - var selectable = new SelectorContext(cssSelector, callbackCtxt); + var selectable = new SelectorContext(cssSelector, callbackCtxt, listContext); if (isPresent(element)) { @@ -215,6 +239,10 @@ export class SelectorMatcher { var classNames = cssSelector.classNames; var attrs = cssSelector.attrs; + for (var i = 0; i < this._listContexts.length; i++) { + this._listContexts[i].alreadyMatched = false; + } + result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result; result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) || result; @@ -282,26 +310,41 @@ export class SelectorMatcher { } +class SelectorListContext { + selectors: List; + alreadyMatched: boolean; + + constructor(selectors:List) { + this.selectors = selectors; + this.alreadyMatched = false; + } +} + // Store context to pass back selector and context when a selector is matched class SelectorContext { selector:CssSelector; notSelector:CssSelector; cbContext; // callback context + listContext: SelectorListContext; - constructor(selector:CssSelector, cbContext) { + constructor(selector:CssSelector, cbContext, listContext: SelectorListContext) { this.selector = selector; this.notSelector = selector.notSelector; this.cbContext = cbContext; + this.listContext = listContext; } finalize(cssSelector: CssSelector, callback) { var result = true; - if (isPresent(this.notSelector)) { + if (isPresent(this.notSelector) && (isBlank(this.listContext) || !this.listContext.alreadyMatched)) { var notMatcher = new SelectorMatcher(); - notMatcher.addSelectable(this.notSelector, null); + notMatcher.addSelectable(this.notSelector, null, null); result = !notMatcher.match(cssSelector, null); } - if (result && isPresent(callback)) { + if (result && isPresent(callback) && (isBlank(this.listContext) || !this.listContext.alreadyMatched)) { + if (isPresent(this.listContext)) { + this.listContext.alreadyMatched = true; + } callback(this.selector, this.cbContext); } return result; diff --git a/modules/angular2/src/dom/parse5_adapter.cjs b/modules/angular2/src/dom/parse5_adapter.cjs index 2bf6dfaaf8..aadf27968c 100644 --- a/modules/angular2/src/dom/parse5_adapter.cjs +++ b/modules/angular2/src/dom/parse5_adapter.cjs @@ -52,7 +52,7 @@ export class Parse5DomAdapter extends DomAdapter { } }; var matcher = new SelectorMatcher(); - matcher.addSelectable(CssSelector.parse(selector)); + matcher.addSelectables(CssSelector.parse(selector)); _recursive(res, el, selector, matcher); return res; } @@ -64,7 +64,7 @@ export class Parse5DomAdapter extends DomAdapter { var result = false; if (matcher == null) { matcher = new SelectorMatcher(); - matcher.addSelectable(CssSelector.parse(selector)); + matcher.addSelectables(CssSelector.parse(selector)); } var cssSelector = new CssSelector(); diff --git a/modules/angular2/test/core/compiler/selector_spec.js b/modules/angular2/test/core/compiler/selector_spec.js index 34db287d5f..a60b07144b 100644 --- a/modules/angular2/test/core/compiler/selector_spec.js +++ b/modules/angular2/test/core/compiler/selector_spec.js @@ -23,162 +23,181 @@ export function main() { }); it('should select by element name case insensitive', () => { - matcher.addSelectable(s1 = CssSelector.parse('someTag'), 1); + matcher.addSelectables(s1 = CssSelector.parse('someTag'), 1); - expect(matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('SOMEOTHERTAG')[0], selectableCollector)).toEqual(false); expect(matched).toEqual([]); - expect(matcher.match(CssSelector.parse('SOMETAG'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1]); + expect(matcher.match(CssSelector.parse('SOMETAG')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1]); }); it('should select by class name case insensitive', () => { - matcher.addSelectable(s1 = CssSelector.parse('.someClass'), 1); - matcher.addSelectable(s2 = CssSelector.parse('.someClass.class2'), 2); + matcher.addSelectables(s1 = CssSelector.parse('.someClass'), 1); + matcher.addSelectables(s2 = CssSelector.parse('.someClass.class2'), 2); - expect(matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('.SOMEOTHERCLASS')[0], selectableCollector)).toEqual(false); expect(matched).toEqual([]); - expect(matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1]); + expect(matcher.match(CssSelector.parse('.SOMECLASS')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1]); reset(); - expect(matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1,s2,2]); + expect(matcher.match(CssSelector.parse('.someClass.class2')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1,s2[0],2]); }); it('should select by attr name case insensitive independent of the value', () => { - matcher.addSelectable(s1 = CssSelector.parse('[someAttr]'), 1); - matcher.addSelectable(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2); + matcher.addSelectables(s1 = CssSelector.parse('[someAttr]'), 1); + matcher.addSelectables(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2); - expect(matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('[SOMEOTHERATTR]')[0], selectableCollector)).toEqual(false); expect(matched).toEqual([]); - expect(matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1]); + expect(matcher.match(CssSelector.parse('[SOMEATTR]')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1]); reset(); - expect(matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1]); + expect(matcher.match(CssSelector.parse('[SOMEATTR=someValue]')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1]); reset(); - expect(matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1,s2,2]); + expect(matcher.match(CssSelector.parse('[someAttr][someAttr2]')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1,s2[0],2]); }); it('should select by attr name only once if the value is from the DOM', () => { - matcher.addSelectable(s1 = CssSelector.parse('[some-decor]'), 1); + matcher.addSelectables(s1 = CssSelector.parse('[some-decor]'), 1); var elementSelector = new CssSelector(); var element = el('
'); var empty = DOM.getAttribute(element, 'attr'); elementSelector.addAttribute('some-decor', empty); matcher.match(elementSelector, selectableCollector); - expect(matched).toEqual([s1,1]); + expect(matched).toEqual([s1[0],1]); }); it('should select by attr name and value case insensitive', () => { - matcher.addSelectable(s1 = CssSelector.parse('[someAttr=someValue]'), 1); + matcher.addSelectables(s1 = CssSelector.parse('[someAttr=someValue]'), 1); - expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]')[0], selectableCollector)).toEqual(false); expect(matched).toEqual([]); - expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1]); + expect(matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],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.addSelectables(s1 = CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1); - expect(matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]')[0], selectableCollector)).toEqual(false); expect(matched).toEqual([]); - expect(matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]')[0], selectableCollector)).toEqual(false); expect(matched).toEqual([]); - expect(matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]')[0], selectableCollector)).toEqual(false); expect(matched).toEqual([]); - expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr]'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr]')[0], selectableCollector)).toEqual(false); expect(matched).toEqual([]); - expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1]); + expect(matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1]); }); it('should select independent of the order in the css selector', () => { - matcher.addSelectable(s1 = CssSelector.parse('[someAttr].someClass'), 1); - matcher.addSelectable(s2 = CssSelector.parse('.someClass[someAttr]'), 2); - matcher.addSelectable(s3 = CssSelector.parse('.class1.class2'), 3); - matcher.addSelectable(s4 = CssSelector.parse('.class2.class1'), 4); + matcher.addSelectables(s1 = CssSelector.parse('[someAttr].someClass'), 1); + matcher.addSelectables(s2 = CssSelector.parse('.someClass[someAttr]'), 2); + matcher.addSelectables(s3 = CssSelector.parse('.class1.class2'), 3); + matcher.addSelectables(s4 = CssSelector.parse('.class2.class1'), 4); - expect(matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1,s2,2]); + expect(matcher.match(CssSelector.parse('[someAttr].someClass')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1,s2[0],2]); reset(); - expect(matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s1,1,s2,2]); + expect(matcher.match(CssSelector.parse('.someClass[someAttr]')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1,s2[0],2]); reset(); - expect(matcher.match(CssSelector.parse('.class1.class2'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s3,3,s4,4]); + expect(matcher.match(CssSelector.parse('.class1.class2')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s3[0],3,s4[0],4]); reset(); - expect(matcher.match(CssSelector.parse('.class2.class1'), selectableCollector)).toEqual(true); - expect(matched).toEqual([s4,4,s3,3]); + expect(matcher.match(CssSelector.parse('.class2.class1')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s4[0],4,s3[0],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); + matcher.addSelectables(CssSelector.parse('p:not(.someClass)'), 1); + matcher.addSelectables(CssSelector.parse('p:not([someAttr])'), 2); + matcher.addSelectables(CssSelector.parse(':not(.someClass)'), 3); + matcher.addSelectables(CssSelector.parse(':not(p)'), 4); + matcher.addSelectables(CssSelector.parse(':not(p[someAttr])'), 5); - expect(matcher.match(CssSelector.parse('p.someClass[someAttr]'), selectableCollector)).toEqual(false); + expect(matcher.match(CssSelector.parse('p.someClass[someAttr]')[0], 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); + matcher.addSelectables(s1 = CssSelector.parse('p:not(.someClass)'), 1); + matcher.addSelectables(s2 = CssSelector.parse('p:not(.someOtherClass[someAttr])'), 2); + matcher.addSelectables(s3 = CssSelector.parse(':not(.someClass)'), 3); + matcher.addSelectables(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]); + expect(matcher.match(CssSelector.parse('p[someOtherAttr].someOtherClass')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1,s2[0],2,s3[0],3,s4[0],4]); + }); + + it('should select with one match in a list', () => { + matcher.addSelectables(s1 = CssSelector.parse('input[type=text], textbox'), 1); + + expect(matcher.match(CssSelector.parse('textbox')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[1],1]); + + reset(); + expect(matcher.match(CssSelector.parse('input[type=text]')[0], selectableCollector)).toEqual(true); + expect(matched).toEqual([s1[0],1]); + }); + + it('should not select twice with two matches in a list', () => { + matcher.addSelectables(s1 = CssSelector.parse('input, .someClass'), 1); + + expect(matcher.match(CssSelector.parse('input.someclass')[0], selectableCollector)).toEqual(true); + expect(matched.length).toEqual(2); + expect(matched).toEqual([s1[0],1]); }); }); describe('CssSelector.parse', () => { it('should detect element names', () => { - var cssSelector = CssSelector.parse('sometag'); + var cssSelector = CssSelector.parse('sometag')[0]; expect(cssSelector.element).toEqual('sometag'); expect(cssSelector.toString()).toEqual('sometag'); }); it('should detect class names', () => { - var cssSelector = CssSelector.parse('.someClass'); + var cssSelector = CssSelector.parse('.someClass')[0]; expect(cssSelector.classNames).toEqual(['someclass']); expect(cssSelector.toString()).toEqual('.someclass'); }); it('should detect attr names', () => { - var cssSelector = CssSelector.parse('[attrname]'); + var cssSelector = CssSelector.parse('[attrname]')[0]; expect(cssSelector.attrs).toEqual(['attrname', '']); expect(cssSelector.toString()).toEqual('[attrname]'); }); it('should detect attr values', () => { - var cssSelector = CssSelector.parse('[attrname=attrvalue]'); + var cssSelector = CssSelector.parse('[attrname=attrvalue]')[0]; expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']); expect(cssSelector.toString()).toEqual('[attrname=attrvalue]'); }); it('should detect multiple parts', () => { - var cssSelector = CssSelector.parse('sometag[attrname=attrvalue].someclass'); + var cssSelector = CssSelector.parse('sometag[attrname=attrvalue].someclass')[0]; expect(cssSelector.element).toEqual('sometag'); expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']); expect(cssSelector.classNames).toEqual(['someclass']); @@ -187,7 +206,7 @@ export function main() { }); it('should detect :not', () => { - var cssSelector = CssSelector.parse('sometag:not([attrname=attrvalue].someclass)'); + var cssSelector = CssSelector.parse('sometag:not([attrname=attrvalue].someclass)')[0]; expect(cssSelector.element).toEqual('sometag'); expect(cssSelector.attrs.length).toEqual(0); expect(cssSelector.classNames.length).toEqual(0); @@ -201,7 +220,7 @@ export function main() { }); it('should detect :not without truthy', () => { - var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)'); + var cssSelector = CssSelector.parse(':not([attrname=attrvalue].someclass)')[0]; expect(cssSelector.element).toEqual("*"); var notSelector = cssSelector.notSelector; @@ -213,8 +232,31 @@ export function main() { it('should throw when nested :not', () => { expect(() => { - CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))') + CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))')[0]; }).toThrowError('Nesting :not is not allowed in a selector'); }); + + it('should detect lists of selectors', () => { + var cssSelectors = CssSelector.parse('.someclass,[attrname=attrvalue], sometag'); + expect(cssSelectors.length).toEqual(3); + + expect(cssSelectors[0].classNames).toEqual(['someclass']); + expect(cssSelectors[1].attrs).toEqual(['attrname', 'attrvalue']); + expect(cssSelectors[2].element).toEqual('sometag'); + }); + + it('should detect lists of selectors with :not', () => { + var cssSelectors = CssSelector.parse('input[type=text], :not(textarea), textbox:not(.special)'); + expect(cssSelectors.length).toEqual(3); + + expect(cssSelectors[0].element).toEqual('input'); + expect(cssSelectors[0].attrs).toEqual(['type', 'text']); + + expect(cssSelectors[1].element).toEqual('*'); + expect(cssSelectors[1].notSelector.element).toEqual('textarea'); + + expect(cssSelectors[2].element).toEqual('textbox'); + expect(cssSelectors[2].notSelector.classNames).toEqual(['special']); + }); }); } \ No newline at end of file diff --git a/modules/benchmarks/src/compiler/selector_benchmark.js b/modules/benchmarks/src/compiler/selector_benchmark.js index 846b1885fe..cdcccff7a0 100644 --- a/modules/benchmarks/src/compiler/selector_benchmark.js +++ b/modules/benchmarks/src/compiler/selector_benchmark.js @@ -20,7 +20,7 @@ export function main() { } fixedMatcher = new SelectorMatcher(); for (var i=0; i { + fixedMatcher.match(fixedSelectors[i][0], (selector, selected) => { matchCount += selected; }); }