
- Don't create intermediate merge results - Only merge embedded ProtoViews that contain `<ng-content>` tags Closes #3150 Closes #3177
402 lines
16 KiB
TypeScript
402 lines
16 KiB
TypeScript
import {DOM} from 'angular2/src/dom/dom_adapter';
|
|
import {isPresent, isBlank, BaseException, isArray} from 'angular2/src/facade/lang';
|
|
import {ListWrapper} from 'angular2/src/facade/collection';
|
|
|
|
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
|
|
import {DomElementBinder} from './element_binder';
|
|
import {RenderProtoViewMergeMapping, RenderProtoViewRef, ViewType} from '../../api';
|
|
import {
|
|
NG_BINDING_CLASS,
|
|
NG_CONTENT_ELEMENT_NAME,
|
|
ClonedProtoView,
|
|
cloneAndQueryProtoView,
|
|
queryBoundElements,
|
|
queryBoundTextNodeIndices,
|
|
NG_SHADOW_ROOT_ELEMENT_NAME
|
|
} from '../util';
|
|
|
|
export function mergeProtoViewsRecursively(protoViewRefs: List<RenderProtoViewRef | List<any>>):
|
|
RenderProtoViewMergeMapping {
|
|
// clone
|
|
var clonedProtoViews = [];
|
|
var hostViewAndBinderIndices: number[][] = [];
|
|
cloneProtoViews(protoViewRefs, clonedProtoViews, hostViewAndBinderIndices);
|
|
var mainProtoView: ClonedProtoView = clonedProtoViews[0];
|
|
|
|
// modify the DOM
|
|
mergeEmbeddedPvsIntoComponentOrRootPv(clonedProtoViews, hostViewAndBinderIndices);
|
|
var fragments = [];
|
|
mergeComponents(clonedProtoViews, hostViewAndBinderIndices, fragments);
|
|
|
|
// create a new root element with the changed fragments and elements
|
|
var rootElement = createRootElementFromFragments(fragments);
|
|
var fragmentsRootNodeCount = fragments.map(fragment => fragment.length);
|
|
var rootNode = DOM.content(rootElement);
|
|
|
|
// read out the new element / text node / ElementBinder order
|
|
var mergedBoundElements = queryBoundElements(rootNode, false);
|
|
var mergedBoundTextIndices: Map<Node, number> = new Map();
|
|
var boundTextNodeMap: Map<Node, any> = indexBoundTextNodes(clonedProtoViews);
|
|
var rootTextNodeIndices =
|
|
calcRootTextNodeIndices(rootNode, boundTextNodeMap, mergedBoundTextIndices);
|
|
var mergedElementBinders = calcElementBinders(clonedProtoViews, mergedBoundElements,
|
|
boundTextNodeMap, mergedBoundTextIndices);
|
|
|
|
// create element / text index mappings
|
|
var mappedElementIndices = calcMappedElementIndices(clonedProtoViews, mergedBoundElements);
|
|
var mappedTextIndices = calcMappedTextIndices(clonedProtoViews, mergedBoundTextIndices);
|
|
|
|
// create result
|
|
var hostElementIndicesByViewIndex =
|
|
calcHostElementIndicesByViewIndex(clonedProtoViews, hostViewAndBinderIndices);
|
|
var nestedViewCounts = calcNestedViewCounts(hostViewAndBinderIndices);
|
|
var mergedProtoView =
|
|
DomProtoView.create(mainProtoView.original.type, rootElement, fragmentsRootNodeCount,
|
|
rootTextNodeIndices, mergedElementBinders);
|
|
return new RenderProtoViewMergeMapping(
|
|
new DomProtoViewRef(mergedProtoView), fragmentsRootNodeCount.length, mappedElementIndices,
|
|
mappedTextIndices, hostElementIndicesByViewIndex, nestedViewCounts);
|
|
}
|
|
|
|
function cloneProtoViews(protoViewRefs: List<RenderProtoViewRef | List<any>>,
|
|
targetClonedProtoViews: ClonedProtoView[],
|
|
targetHostViewAndBinderIndices: number[][]) {
|
|
var hostProtoView = resolveInternalDomProtoView(protoViewRefs[0]);
|
|
var hostPvIdx = targetClonedProtoViews.length;
|
|
targetClonedProtoViews.push(cloneAndQueryProtoView(hostProtoView, false));
|
|
if (targetHostViewAndBinderIndices.length === 0) {
|
|
targetHostViewAndBinderIndices.push([null, null]);
|
|
}
|
|
var protoViewIdx = 1;
|
|
for (var i = 0; i < hostProtoView.elementBinders.length; i++) {
|
|
var binder = hostProtoView.elementBinders[i];
|
|
if (binder.hasNestedProtoView) {
|
|
var nestedEntry = protoViewRefs[protoViewIdx++];
|
|
if (isPresent(nestedEntry)) {
|
|
targetHostViewAndBinderIndices.push([hostPvIdx, i]);
|
|
if (isArray(nestedEntry)) {
|
|
cloneProtoViews(<any[]>nestedEntry, targetClonedProtoViews,
|
|
targetHostViewAndBinderIndices);
|
|
} else {
|
|
targetClonedProtoViews.push(
|
|
cloneAndQueryProtoView(resolveInternalDomProtoView(nestedEntry), false));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function indexBoundTextNodes(mergableProtoViews: ClonedProtoView[]): Map<Node, any> {
|
|
var boundTextNodeMap = new Map();
|
|
for (var pvIndex = 0; pvIndex < mergableProtoViews.length; pvIndex++) {
|
|
var mergableProtoView = mergableProtoViews[pvIndex];
|
|
mergableProtoView.boundTextNodes.forEach(
|
|
(textNode) => { boundTextNodeMap.set(textNode, null); });
|
|
}
|
|
return boundTextNodeMap;
|
|
}
|
|
|
|
function mergeEmbeddedPvsIntoComponentOrRootPv(clonedProtoViews: ClonedProtoView[],
|
|
hostViewAndBinderIndices: number[][]) {
|
|
var nearestHostComponentOrRootPvIndices =
|
|
calcNearestHostComponentOrRootPvIndices(clonedProtoViews, hostViewAndBinderIndices);
|
|
for (var viewIdx = 1; viewIdx < clonedProtoViews.length; viewIdx++) {
|
|
var clonedProtoView = clonedProtoViews[viewIdx];
|
|
if (clonedProtoView.original.type === ViewType.EMBEDDED) {
|
|
var hostComponentIdx = nearestHostComponentOrRootPvIndices[viewIdx];
|
|
var hostPv = clonedProtoViews[hostComponentIdx];
|
|
clonedProtoView.fragments.forEach((fragment) => hostPv.fragments.push(fragment));
|
|
}
|
|
}
|
|
}
|
|
|
|
function calcNearestHostComponentOrRootPvIndices(clonedProtoViews: ClonedProtoView[],
|
|
hostViewAndBinderIndices: number[][]): number[] {
|
|
var nearestHostComponentOrRootPvIndices = ListWrapper.createFixedSize(clonedProtoViews.length);
|
|
nearestHostComponentOrRootPvIndices[0] = null;
|
|
for (var viewIdx = 1; viewIdx < hostViewAndBinderIndices.length; viewIdx++) {
|
|
var hostViewIdx = hostViewAndBinderIndices[viewIdx][0];
|
|
var hostProtoView = clonedProtoViews[hostViewIdx];
|
|
if (hostViewIdx === 0 || hostProtoView.original.type === ViewType.COMPONENT) {
|
|
nearestHostComponentOrRootPvIndices[viewIdx] = hostViewIdx;
|
|
} else {
|
|
nearestHostComponentOrRootPvIndices[viewIdx] =
|
|
nearestHostComponentOrRootPvIndices[hostViewIdx];
|
|
}
|
|
}
|
|
return nearestHostComponentOrRootPvIndices;
|
|
}
|
|
|
|
function mergeComponents(clonedProtoViews: ClonedProtoView[], hostViewAndBinderIndices: number[][],
|
|
targetFragments: Node[][]) {
|
|
var hostProtoView = clonedProtoViews[0];
|
|
hostProtoView.fragments.forEach((fragment) => targetFragments.push(fragment));
|
|
|
|
for (var viewIdx = 1; viewIdx < clonedProtoViews.length; viewIdx++) {
|
|
var hostViewIdx = hostViewAndBinderIndices[viewIdx][0];
|
|
var hostBinderIdx = hostViewAndBinderIndices[viewIdx][1];
|
|
var hostProtoView = clonedProtoViews[hostViewIdx];
|
|
var clonedProtoView = clonedProtoViews[viewIdx];
|
|
if (clonedProtoView.original.type === ViewType.COMPONENT) {
|
|
mergeComponent(hostProtoView, hostBinderIdx, clonedProtoView, targetFragments);
|
|
}
|
|
}
|
|
}
|
|
|
|
function mergeComponent(hostProtoView: ClonedProtoView, binderIdx: number,
|
|
nestedProtoView: ClonedProtoView, targetFragments: Node[][]) {
|
|
var hostElement = hostProtoView.boundElements[binderIdx];
|
|
|
|
// We wrap the fragments into elements so that we can expand <ng-content>
|
|
// even for root nodes in the fragment without special casing them.
|
|
var fragmentElements = mapFragmentsIntoElements(nestedProtoView.fragments);
|
|
var contentElements = findContentElements(fragmentElements);
|
|
|
|
var projectableNodes = DOM.childNodesAsList(hostElement);
|
|
for (var i = 0; i < contentElements.length; i++) {
|
|
var contentElement = contentElements[i];
|
|
var select = DOM.getAttribute(contentElement, 'select');
|
|
projectableNodes = projectMatchingNodes(select, contentElement, projectableNodes);
|
|
}
|
|
|
|
// unwrap the fragment elements into arrays of nodes after projecting
|
|
var fragments = extractFragmentNodesFromElements(fragmentElements);
|
|
appendComponentNodesToHost(hostProtoView, binderIdx, fragments[0]);
|
|
|
|
for (var i = 1; i < fragments.length; i++) {
|
|
targetFragments.push(fragments[i]);
|
|
}
|
|
}
|
|
|
|
function mapFragmentsIntoElements(fragments: Node[][]): Element[] {
|
|
return fragments.map(fragment => {
|
|
var fragmentElement = DOM.createTemplate('');
|
|
fragment.forEach(node => DOM.appendChild(DOM.content(fragmentElement), node));
|
|
return fragmentElement;
|
|
});
|
|
}
|
|
|
|
function extractFragmentNodesFromElements(fragmentElements: Element[]): Node[][] {
|
|
return fragmentElements.map(
|
|
(fragmentElement) => { return DOM.childNodesAsList(DOM.content(fragmentElement)); });
|
|
}
|
|
|
|
function findContentElements(fragmentElements: Element[]): Element[] {
|
|
var contentElements = [];
|
|
fragmentElements.forEach((fragmentElement: Element) => {
|
|
var fragmentContentElements =
|
|
DOM.querySelectorAll(DOM.content(fragmentElement), NG_CONTENT_ELEMENT_NAME);
|
|
for (var i = 0; i < fragmentContentElements.length; i++) {
|
|
contentElements.push(fragmentContentElements[i]);
|
|
}
|
|
});
|
|
return sortContentElements(contentElements);
|
|
}
|
|
|
|
function appendComponentNodesToHost(hostProtoView: ClonedProtoView, binderIdx: number,
|
|
componentRootNodes: Node[]) {
|
|
var hostElement = hostProtoView.boundElements[binderIdx];
|
|
var binder = hostProtoView.original.elementBinders[binderIdx];
|
|
if (binder.hasNativeShadowRoot) {
|
|
var shadowRootWrapper = DOM.createElement(NG_SHADOW_ROOT_ELEMENT_NAME);
|
|
for (var i = 0; i < componentRootNodes.length; i++) {
|
|
DOM.appendChild(shadowRootWrapper, componentRootNodes[i]);
|
|
}
|
|
var firstChild = DOM.firstChild(hostElement);
|
|
if (isPresent(firstChild)) {
|
|
DOM.insertBefore(firstChild, shadowRootWrapper);
|
|
} else {
|
|
DOM.appendChild(hostElement, shadowRootWrapper);
|
|
}
|
|
} else {
|
|
DOM.clearNodes(hostElement);
|
|
for (var i = 0; i < componentRootNodes.length; i++) {
|
|
DOM.appendChild(hostElement, componentRootNodes[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function projectMatchingNodes(selector: string, contentElement: Element, nodes: Node[]): Node[] {
|
|
var remaining = [];
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var node = nodes[i];
|
|
var matches = false;
|
|
if (isWildcard(selector)) {
|
|
matches = true;
|
|
} else if (DOM.isElementNode(node) && DOM.elementMatches(node, selector)) {
|
|
matches = true;
|
|
}
|
|
if (matches) {
|
|
DOM.insertBefore(contentElement, node);
|
|
} else {
|
|
remaining.push(node);
|
|
}
|
|
}
|
|
DOM.remove(contentElement);
|
|
return remaining;
|
|
}
|
|
|
|
function isWildcard(selector): boolean {
|
|
return isBlank(selector) || selector.length === 0 || selector == '*';
|
|
}
|
|
|
|
// we need to sort content elements as they can originate from
|
|
// different sub views
|
|
function sortContentElements(contentElements: Element[]): Element[] {
|
|
// for now, only move the wildcard selector to the end.
|
|
// TODO(tbosch): think about sorting by selector specifity...
|
|
var firstWildcard = null;
|
|
var sorted = [];
|
|
contentElements.forEach((contentElement) => {
|
|
var select = DOM.getAttribute(contentElement, 'select');
|
|
if (isWildcard(select)) {
|
|
if (isBlank(firstWildcard)) {
|
|
firstWildcard = contentElement;
|
|
}
|
|
} else {
|
|
sorted.push(contentElement);
|
|
}
|
|
});
|
|
if (isPresent(firstWildcard)) {
|
|
sorted.push(firstWildcard);
|
|
}
|
|
return sorted;
|
|
}
|
|
|
|
|
|
function createRootElementFromFragments(fragments: Node[][]): Element {
|
|
var rootElement = DOM.createTemplate('');
|
|
var rootNode = DOM.content(rootElement);
|
|
fragments.forEach(
|
|
(fragment) => { fragment.forEach((node) => { DOM.appendChild(rootNode, node); }); });
|
|
return rootElement;
|
|
}
|
|
|
|
function calcRootTextNodeIndices(rootNode: Node, boundTextNodes: Map<Node, any>,
|
|
targetBoundTextIndices: Map<Node, number>): number[] {
|
|
var rootTextNodeIndices = [];
|
|
queryBoundTextNodeIndices(rootNode, boundTextNodes, (textNode, nodeIndex, _) => {
|
|
rootTextNodeIndices.push(nodeIndex);
|
|
targetBoundTextIndices.set(textNode, targetBoundTextIndices.size);
|
|
});
|
|
return rootTextNodeIndices;
|
|
}
|
|
|
|
function calcElementBinders(clonedProtoViews: ClonedProtoView[], mergedBoundElements: Element[],
|
|
boundTextNodes: Map<Node, any>,
|
|
targetBoundTextIndices: Map<Node, number>): DomElementBinder[] {
|
|
var elementBinderByElement: Map<Element, DomElementBinder> =
|
|
indexElementBindersByElement(clonedProtoViews);
|
|
var mergedElementBinders = [];
|
|
for (var i = 0; i < mergedBoundElements.length; i++) {
|
|
var element = mergedBoundElements[i];
|
|
var textNodeIndices = [];
|
|
queryBoundTextNodeIndices(element, boundTextNodes, (textNode, nodeIndex, _) => {
|
|
textNodeIndices.push(nodeIndex);
|
|
targetBoundTextIndices.set(textNode, targetBoundTextIndices.size);
|
|
});
|
|
mergedElementBinders.push(
|
|
updateElementBinderTextNodeIndices(elementBinderByElement.get(element), textNodeIndices));
|
|
}
|
|
return mergedElementBinders;
|
|
}
|
|
|
|
function indexElementBindersByElement(mergableProtoViews: ClonedProtoView[]):
|
|
Map<Element, DomElementBinder> {
|
|
var elementBinderByElement = new Map();
|
|
mergableProtoViews.forEach((mergableProtoView) => {
|
|
for (var i = 0; i < mergableProtoView.boundElements.length; i++) {
|
|
var el = mergableProtoView.boundElements[i];
|
|
if (isPresent(el)) {
|
|
elementBinderByElement.set(el, mergableProtoView.original.elementBinders[i]);
|
|
}
|
|
}
|
|
});
|
|
return elementBinderByElement;
|
|
}
|
|
|
|
function updateElementBinderTextNodeIndices(elementBinder: DomElementBinder,
|
|
textNodeIndices: number[]): DomElementBinder {
|
|
var result;
|
|
if (isBlank(elementBinder)) {
|
|
result = new DomElementBinder({
|
|
textNodeIndices: textNodeIndices,
|
|
hasNestedProtoView: false,
|
|
eventLocals: null,
|
|
localEvents: [],
|
|
globalEvents: [],
|
|
hasNativeShadowRoot: null
|
|
});
|
|
} else {
|
|
result = new DomElementBinder({
|
|
textNodeIndices: textNodeIndices,
|
|
hasNestedProtoView: false,
|
|
eventLocals: elementBinder.eventLocals,
|
|
localEvents: elementBinder.localEvents,
|
|
globalEvents: elementBinder.globalEvents,
|
|
hasNativeShadowRoot: elementBinder.hasNativeShadowRoot
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function calcMappedElementIndices(clonedProtoViews: ClonedProtoView[],
|
|
mergedBoundElements: Element[]): number[] {
|
|
var mergedBoundElementIndices: Map<Element, number> = indexArray(mergedBoundElements);
|
|
var mappedElementIndices = [];
|
|
clonedProtoViews.forEach((clonedProtoView) => {
|
|
clonedProtoView.boundElements.forEach((boundElement) => {
|
|
var mappedElementIndex = mergedBoundElementIndices.get(boundElement);
|
|
mappedElementIndices.push(mappedElementIndex);
|
|
});
|
|
});
|
|
return mappedElementIndices;
|
|
}
|
|
|
|
function calcMappedTextIndices(clonedProtoViews: ClonedProtoView[],
|
|
mergedBoundTextIndices: Map<Node, number>): number[] {
|
|
var mappedTextIndices = [];
|
|
clonedProtoViews.forEach((clonedProtoView) => {
|
|
clonedProtoView.boundTextNodes.forEach((textNode) => {
|
|
var mappedTextIndex = mergedBoundTextIndices.get(textNode);
|
|
mappedTextIndices.push(mappedTextIndex);
|
|
});
|
|
});
|
|
return mappedTextIndices;
|
|
}
|
|
|
|
function calcHostElementIndicesByViewIndex(clonedProtoViews: ClonedProtoView[],
|
|
hostViewAndBinderIndices: number[][]): number[] {
|
|
var hostElementIndices = [null];
|
|
var viewElementOffsets = [0];
|
|
var elementIndex = clonedProtoViews[0].original.elementBinders.length;
|
|
for (var viewIdx = 1; viewIdx < hostViewAndBinderIndices.length; viewIdx++) {
|
|
viewElementOffsets.push(elementIndex);
|
|
elementIndex += clonedProtoViews[viewIdx].original.elementBinders.length;
|
|
var hostViewIdx = hostViewAndBinderIndices[viewIdx][0];
|
|
var hostBinderIdx = hostViewAndBinderIndices[viewIdx][1];
|
|
hostElementIndices.push(viewElementOffsets[hostViewIdx] + hostBinderIdx);
|
|
}
|
|
return hostElementIndices;
|
|
}
|
|
|
|
function calcNestedViewCounts(hostViewAndBinderIndices: number[][]): number[] {
|
|
var nestedViewCounts = ListWrapper.createFixedSize(hostViewAndBinderIndices.length);
|
|
ListWrapper.fill(nestedViewCounts, 0);
|
|
for (var viewIdx = hostViewAndBinderIndices.length - 1; viewIdx >= 1; viewIdx--) {
|
|
var hostViewAndElementIdx = hostViewAndBinderIndices[viewIdx];
|
|
if (isPresent(hostViewAndElementIdx)) {
|
|
nestedViewCounts[hostViewAndElementIdx[0]] += nestedViewCounts[viewIdx] + 1;
|
|
}
|
|
}
|
|
return nestedViewCounts;
|
|
}
|
|
|
|
function indexArray(arr: any[]): Map<any, number> {
|
|
var map = new Map();
|
|
for (var i = 0; i < arr.length; i++) {
|
|
map.set(arr[i], i);
|
|
}
|
|
return map;
|
|
}
|