diff --git a/packages/elements/src/extract-projectable-nodes.ts b/packages/elements/src/extract-projectable-nodes.ts new file mode 100644 index 0000000000..b6df05bcba --- /dev/null +++ b/packages/elements/src/extract-projectable-nodes.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// NOTE: This is a (slightly improved) version of what is used in ngUpgrade's +// `DowngradeComponentAdapter`. +// TODO(gkalpak): Investigate if it makes sense to share the code. + +import {isElement, matchesSelector} from './utils'; + +export function extractProjectableNodes(host: HTMLElement, ngContentSelectors: string[]): Node[][] { + const nodes = host.childNodes; + const projectableNodes: Node[][] = ngContentSelectors.map(() => []); + let wildcardIndex = -1; + + ngContentSelectors.some((selector, i) => { + if (selector === '*') { + wildcardIndex = i; + return true; + } + return false; + }); + + for (let i = 0, ii = nodes.length; i < ii; ++i) { + const node = nodes[i]; + const ngContentIndex = findMatchingIndex(node, ngContentSelectors, wildcardIndex); + + if (ngContentIndex !== -1) { + projectableNodes[ngContentIndex].push(node); + } + } + + return projectableNodes; +} + +function findMatchingIndex(node: Node, selectors: string[], defaultIndex: number): number { + let matchingIndex = defaultIndex; + + if (isElement(node)) { + selectors.some((selector, i) => { + if ((selector !== '*') && matchesSelector(node, selector)) { + matchingIndex = i; + return true; + } + return false; + }); + } + + return matchingIndex; +} diff --git a/packages/elements/test/extract-projectable-nodes_spec.ts b/packages/elements/test/extract-projectable-nodes_spec.ts new file mode 100644 index 0000000000..9d79760916 --- /dev/null +++ b/packages/elements/test/extract-projectable-nodes_spec.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {extractProjectableNodes} from '../src/extract-projectable-nodes'; + +export function main() { + describe('extractProjectableNodes()', () => { + let elem: HTMLElement; + let childNodes: NodeList; + + const expectProjectableNodes = (matches: {[selector: string]: number[]}) => { + const selectors = Object.keys(matches); + const expected = selectors.map(selector => { + const matchingIndices = matches[selector]; + return matchingIndices.map(idx => childNodes[idx]); + }); + + expect(extractProjectableNodes(elem, selectors)).toEqual(expected); + }; + const test = (matches: {[selector: string]: number[]}) => () => expectProjectableNodes(matches); + + beforeEach(() => { + elem = document.createElement('div'); + elem.innerHTML = '
' + + '' + + '
' + + '' + + '' + + 'Text' + + '' + + 'More text'; + childNodes = Array.prototype.slice.call(elem.childNodes); + }); + + it('should match each node to the corresponding selector', test({ + '[first]': [0], + '#bar': [1], + '#quux': [4], + })); + + it('should ignore non-matching nodes', test({ + '.zoo': [], + })); + + it('should only match top-level child nodes', test({ + 'span': [1], + '.bar': [], + })); + + it('should support complex selectors', test({ + '.foo:not(div)': [4], + 'div + #bar': [1], + })); + + it('should match each node with the first matching selector', test({ + 'div': [0], + '.foo': [4], + 'blink': [], + })); + + describe('(with wildcard selector)', () => { + it('should match non-element nodes to `*` (but still ignore comments)', test({ + 'div,span,blink': [0, 1, 4], + '*': [2, 3, 5], + })); + + it('should match otherwise unmatched nodes to `*`', test({ + 'div,blink': [0, 4], + '*': [1, 2, 3, 5], + })); + + it('should give higher priority to `*` (eve if it appears first)', test({ + '*': [2, 3, 5], + 'div,span,blink': [0, 1, 4], + })); + }); + }); +}