
Also inserts comment nodes before/after projected nodes so that text nodes don’t get merged when we serialize/deserialize them. Closes #3356 First part of #3364
185 lines
6.9 KiB
TypeScript
185 lines
6.9 KiB
TypeScript
import {StringWrapper, isPresent, isBlank} from 'angular2/src/facade/lang';
|
|
import {DOM} from 'angular2/src/dom/dom_adapter';
|
|
import {ListWrapper} from 'angular2/src/facade/collection';
|
|
import {DomProtoView} from './view/proto_view';
|
|
import {DomElementBinder} from './view/element_binder';
|
|
|
|
export const NG_BINDING_CLASS_SELECTOR = '.ng-binding';
|
|
export const NG_BINDING_CLASS = 'ng-binding';
|
|
|
|
export const EVENT_TARGET_SEPARATOR = ':';
|
|
|
|
export const NG_CONTENT_ELEMENT_NAME = 'ng-content';
|
|
export const NG_SHADOW_ROOT_ELEMENT_NAME = 'shadow-root';
|
|
|
|
const MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE = 20;
|
|
|
|
var CAMEL_CASE_REGEXP = /([A-Z])/g;
|
|
var DASH_CASE_REGEXP = /-([a-z])/g;
|
|
|
|
|
|
export function camelCaseToDashCase(input: string): string {
|
|
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP,
|
|
(m) => { return '-' + m[1].toLowerCase(); });
|
|
}
|
|
|
|
export function dashCaseToCamelCase(input: string): string {
|
|
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP,
|
|
(m) => { return m[1].toUpperCase(); });
|
|
}
|
|
|
|
// Attention: This is on the hot path, so don't use closures or default values!
|
|
export function queryBoundElements(templateContent: Node, isSingleElementChild: boolean):
|
|
Element[] {
|
|
var result;
|
|
var dynamicElementList;
|
|
var elementIdx = 0;
|
|
if (isSingleElementChild) {
|
|
var rootElement = DOM.firstChild(templateContent);
|
|
var rootHasBinding = DOM.hasClass(rootElement, NG_BINDING_CLASS);
|
|
dynamicElementList = DOM.getElementsByClassName(rootElement, NG_BINDING_CLASS);
|
|
result = ListWrapper.createFixedSize(dynamicElementList.length + (rootHasBinding ? 1 : 0));
|
|
if (rootHasBinding) {
|
|
result[elementIdx++] = rootElement;
|
|
}
|
|
} else {
|
|
dynamicElementList = DOM.querySelectorAll(templateContent, NG_BINDING_CLASS_SELECTOR);
|
|
result = ListWrapper.createFixedSize(dynamicElementList.length);
|
|
}
|
|
for (var i = 0; i < dynamicElementList.length; i++) {
|
|
result[elementIdx++] = dynamicElementList[i];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export class ClonedProtoView {
|
|
constructor(public original: DomProtoView, public fragments: Node[][],
|
|
public boundElements: Element[], public boundTextNodes: Node[]) {}
|
|
}
|
|
|
|
export function cloneAndQueryProtoView(pv: DomProtoView, importIntoDocument: boolean):
|
|
ClonedProtoView {
|
|
var templateContent = pv.cloneableTemplate.clone(importIntoDocument);
|
|
|
|
var boundElements = queryBoundElements(templateContent, pv.isSingleElementFragment);
|
|
var boundTextNodes = queryBoundTextNodes(templateContent, pv.rootTextNodeIndices, boundElements,
|
|
pv.elementBinders, pv.boundTextNodeCount);
|
|
|
|
var fragments = queryFragments(templateContent, pv.fragmentsRootNodeCount);
|
|
return new ClonedProtoView(pv, fragments, boundElements, boundTextNodes);
|
|
}
|
|
|
|
function queryFragments(templateContent: Node, fragmentsRootNodeCount: number[]): Node[][] {
|
|
var fragments = ListWrapper.createGrowableSize(fragmentsRootNodeCount.length);
|
|
|
|
// Note: An explicit loop is the fastest way to convert a DOM array into a JS array!
|
|
var childNode = DOM.firstChild(templateContent);
|
|
for (var fragmentIndex = 0; fragmentIndex < fragments.length; fragmentIndex++) {
|
|
var fragment = ListWrapper.createFixedSize(fragmentsRootNodeCount[fragmentIndex]);
|
|
fragments[fragmentIndex] = fragment;
|
|
for (var i = 0; i < fragment.length; i++) {
|
|
fragment[i] = childNode;
|
|
childNode = DOM.nextSibling(childNode);
|
|
}
|
|
}
|
|
return fragments;
|
|
}
|
|
|
|
function queryBoundTextNodes(templateContent: Node, rootTextNodeIndices: number[],
|
|
boundElements: Element[], elementBinders: DomElementBinder[],
|
|
boundTextNodeCount: number): Node[] {
|
|
var boundTextNodes = ListWrapper.createFixedSize(boundTextNodeCount);
|
|
var textNodeIndex = 0;
|
|
if (rootTextNodeIndices.length > 0) {
|
|
var rootChildNodes = DOM.childNodes(templateContent);
|
|
for (var i = 0; i < rootTextNodeIndices.length; i++) {
|
|
boundTextNodes[textNodeIndex++] = rootChildNodes[rootTextNodeIndices[i]];
|
|
}
|
|
}
|
|
for (var i = 0; i < elementBinders.length; i++) {
|
|
var binder = elementBinders[i];
|
|
var element: Node = boundElements[i];
|
|
if (binder.textNodeIndices.length > 0) {
|
|
var childNodes = DOM.childNodes(element);
|
|
for (var j = 0; j < binder.textNodeIndices.length; j++) {
|
|
boundTextNodes[textNodeIndex++] = childNodes[binder.textNodeIndices[j]];
|
|
}
|
|
}
|
|
}
|
|
return boundTextNodes;
|
|
}
|
|
|
|
|
|
export function isElementWithTag(node: Node, elementName: string): boolean {
|
|
return DOM.isElementNode(node) && DOM.tagName(node).toLowerCase() == elementName.toLowerCase();
|
|
}
|
|
|
|
export function queryBoundTextNodeIndices(parentNode: Node, boundTextNodes: Map<Node, any>,
|
|
resultCallback: Function) {
|
|
var childNodes = DOM.childNodes(parentNode);
|
|
for (var j = 0; j < childNodes.length; j++) {
|
|
var node = childNodes[j];
|
|
if (boundTextNodes.has(node)) {
|
|
resultCallback(node, j, boundTextNodes.get(node));
|
|
}
|
|
}
|
|
}
|
|
|
|
export function prependAll(parentNode: Node, nodes: Node[]) {
|
|
var lastInsertedNode = null;
|
|
nodes.forEach(node => {
|
|
if (isBlank(lastInsertedNode)) {
|
|
var firstChild = DOM.firstChild(parentNode);
|
|
if (isPresent(firstChild)) {
|
|
DOM.insertBefore(firstChild, node);
|
|
} else {
|
|
DOM.appendChild(parentNode, node);
|
|
}
|
|
} else {
|
|
DOM.insertAfter(lastInsertedNode, node);
|
|
}
|
|
lastInsertedNode = node;
|
|
});
|
|
}
|
|
|
|
export interface CloneableTemplate { clone(importIntoDoc: boolean): Node; }
|
|
|
|
export class SerializedCloneableTemplate implements CloneableTemplate {
|
|
templateString: string;
|
|
constructor(templateRoot: Element) { this.templateString = DOM.getInnerHTML(templateRoot); }
|
|
clone(importIntoDoc: boolean): Node {
|
|
var result = DOM.content(DOM.createTemplate(this.templateString));
|
|
if (importIntoDoc) {
|
|
result = DOM.adoptNode(result);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
export class ReferenceCloneableTemplate implements CloneableTemplate {
|
|
constructor(public templateRoot: Element) {}
|
|
clone(importIntoDoc: boolean): Node {
|
|
if (importIntoDoc) {
|
|
return DOM.importIntoDoc(DOM.content(this.templateRoot));
|
|
} else {
|
|
return DOM.clone(DOM.content(this.templateRoot));
|
|
}
|
|
}
|
|
}
|
|
|
|
export function prepareTemplateForClone(templateRoot: Element): CloneableTemplate {
|
|
var root = DOM.content(templateRoot);
|
|
var elementCount = DOM.querySelectorAll(root, '*').length;
|
|
var firstChild = DOM.firstChild(root);
|
|
var forceSerialize =
|
|
isPresent(firstChild) && DOM.isCommentNode(firstChild) ? DOM.nodeValue(firstChild) : null;
|
|
if (forceSerialize == 'nocache') {
|
|
return new SerializedCloneableTemplate(templateRoot);
|
|
} else if (forceSerialize == 'cache') {
|
|
return new ReferenceCloneableTemplate(templateRoot);
|
|
} else if (elementCount > MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE) {
|
|
return new SerializedCloneableTemplate(templateRoot);
|
|
} else {
|
|
return new ReferenceCloneableTemplate(templateRoot);
|
|
}
|
|
} |