chore: move core modules into core directory
BREAKING CHANGE: This change moves the http module into angular2/, so its import path is now angular2/http instead of http/http. Many other modules have also been moved around inside of angular2, but the public API paths have not changed as of this commit.
This commit is contained in:
509
modules/angular2/src/core/render/api.ts
Normal file
509
modules/angular2/src/core/render/api.ts
Normal file
@ -0,0 +1,509 @@
|
||||
import {isPresent, isBlank, RegExpWrapper} from 'angular2/src/facade/lang';
|
||||
import {Promise} from 'angular2/src/facade/async';
|
||||
import {List, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {ASTWithSource} from 'angular2/src/change_detection/change_detection';
|
||||
|
||||
/**
|
||||
* General notes:
|
||||
*
|
||||
* The methods for creating / destroying views in this API are used in the AppViewHydrator
|
||||
* and RenderViewHydrator as well.
|
||||
*
|
||||
* We are already parsing expressions on the render side:
|
||||
* - this makes the ElementBinders more compact
|
||||
* (e.g. no need to distinguish interpolations from regular expressions from literals)
|
||||
* - allows to retrieve which properties should be accessed from the event
|
||||
* by looking at the expression
|
||||
* - we need the parse at least for the `template` attribute to match
|
||||
* directives in it
|
||||
* - render compiler is not on the critical path as
|
||||
* its output will be stored in precompiled templates.
|
||||
*/
|
||||
|
||||
export class EventBinding {
|
||||
constructor(public fullName: string, public source: ASTWithSource) {}
|
||||
}
|
||||
|
||||
export enum PropertyBindingType {
|
||||
PROPERTY,
|
||||
ATTRIBUTE,
|
||||
CLASS,
|
||||
STYLE
|
||||
}
|
||||
|
||||
export class ElementPropertyBinding {
|
||||
constructor(public type: PropertyBindingType, public astWithSource: ASTWithSource,
|
||||
public property: string, public unit: string = null) {}
|
||||
}
|
||||
|
||||
export class RenderElementBinder {
|
||||
index: number;
|
||||
parentIndex: number;
|
||||
distanceToParent: number;
|
||||
directives: List<DirectiveBinder>;
|
||||
nestedProtoView: ProtoViewDto;
|
||||
propertyBindings: List<ElementPropertyBinding>;
|
||||
variableBindings: Map<string, string>;
|
||||
// Note: this contains a preprocessed AST
|
||||
// that replaced the values that should be extracted from the element
|
||||
// with a local name
|
||||
eventBindings: List<EventBinding>;
|
||||
readAttributes: Map<string, string>;
|
||||
|
||||
constructor({index, parentIndex, distanceToParent, directives, nestedProtoView, propertyBindings,
|
||||
variableBindings, eventBindings, readAttributes}: {
|
||||
index?: number,
|
||||
parentIndex?: number,
|
||||
distanceToParent?: number,
|
||||
directives?: List<DirectiveBinder>,
|
||||
nestedProtoView?: ProtoViewDto,
|
||||
propertyBindings?: List<ElementPropertyBinding>,
|
||||
variableBindings?: Map<string, string>,
|
||||
eventBindings?: List<EventBinding>,
|
||||
readAttributes?: Map<string, string>
|
||||
} = {}) {
|
||||
this.index = index;
|
||||
this.parentIndex = parentIndex;
|
||||
this.distanceToParent = distanceToParent;
|
||||
this.directives = directives;
|
||||
this.nestedProtoView = nestedProtoView;
|
||||
this.propertyBindings = propertyBindings;
|
||||
this.variableBindings = variableBindings;
|
||||
this.eventBindings = eventBindings;
|
||||
this.readAttributes = readAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectiveBinder {
|
||||
// Index into the array of directives in the View instance
|
||||
directiveIndex: number;
|
||||
propertyBindings: Map<string, ASTWithSource>;
|
||||
// Note: this contains a preprocessed AST
|
||||
// that replaced the values that should be extracted from the element
|
||||
// with a local name
|
||||
eventBindings: List<EventBinding>;
|
||||
hostPropertyBindings: List<ElementPropertyBinding>;
|
||||
constructor({directiveIndex, propertyBindings, eventBindings, hostPropertyBindings}: {
|
||||
directiveIndex?: number,
|
||||
propertyBindings?: Map<string, ASTWithSource>,
|
||||
eventBindings?: List<EventBinding>,
|
||||
hostPropertyBindings?: List<ElementPropertyBinding>
|
||||
}) {
|
||||
this.directiveIndex = directiveIndex;
|
||||
this.propertyBindings = propertyBindings;
|
||||
this.eventBindings = eventBindings;
|
||||
this.hostPropertyBindings = hostPropertyBindings;
|
||||
}
|
||||
}
|
||||
|
||||
export enum ViewType {
|
||||
// A view that contains the host element with bound component directive.
|
||||
// Contains a COMPONENT view
|
||||
HOST,
|
||||
// The view of the component
|
||||
// Can contain 0 to n EMBEDDED views
|
||||
COMPONENT,
|
||||
// A view that is embedded into another View via a <template> element
|
||||
// inside of a COMPONENT view
|
||||
EMBEDDED
|
||||
}
|
||||
|
||||
export class ProtoViewDto {
|
||||
render: RenderProtoViewRef;
|
||||
elementBinders: List<RenderElementBinder>;
|
||||
variableBindings: Map<string, string>;
|
||||
type: ViewType;
|
||||
textBindings: List<ASTWithSource>;
|
||||
transitiveNgContentCount: number;
|
||||
|
||||
constructor({render, elementBinders, variableBindings, type, textBindings,
|
||||
transitiveNgContentCount}: {
|
||||
render?: RenderProtoViewRef,
|
||||
elementBinders?: List<RenderElementBinder>,
|
||||
variableBindings?: Map<string, string>,
|
||||
type?: ViewType,
|
||||
textBindings?: List<ASTWithSource>,
|
||||
transitiveNgContentCount?: number
|
||||
}) {
|
||||
this.render = render;
|
||||
this.elementBinders = elementBinders;
|
||||
this.variableBindings = variableBindings;
|
||||
this.type = type;
|
||||
this.textBindings = textBindings;
|
||||
this.transitiveNgContentCount = transitiveNgContentCount;
|
||||
}
|
||||
}
|
||||
|
||||
export class RenderDirectiveMetadata {
|
||||
static get DIRECTIVE_TYPE() { return 0; }
|
||||
static get COMPONENT_TYPE() { return 1; }
|
||||
id: any;
|
||||
selector: string;
|
||||
compileChildren: boolean;
|
||||
events: List<string>;
|
||||
properties: List<string>;
|
||||
readAttributes: List<string>;
|
||||
type: number;
|
||||
callOnDestroy: boolean;
|
||||
callOnChange: boolean;
|
||||
callOnCheck: boolean;
|
||||
callOnInit: boolean;
|
||||
callOnAllChangesDone: boolean;
|
||||
changeDetection: string;
|
||||
exportAs: string;
|
||||
hostListeners: Map<string, string>;
|
||||
hostProperties: Map<string, string>;
|
||||
hostAttributes: Map<string, string>;
|
||||
hostActions: Map<string, string>;
|
||||
// group 1: "property" from "[property]"
|
||||
// group 2: "event" from "(event)"
|
||||
// group 3: "action" from "@action"
|
||||
private static _hostRegExp = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\))|(?:@(.+)))$/g;
|
||||
|
||||
constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes,
|
||||
hostActions, properties, readAttributes, type, callOnDestroy, callOnChange,
|
||||
callOnCheck, callOnInit, callOnAllChangesDone, changeDetection, exportAs}: {
|
||||
id?: string,
|
||||
selector?: string,
|
||||
compileChildren?: boolean,
|
||||
events?: List<string>,
|
||||
hostListeners?: Map<string, string>,
|
||||
hostProperties?: Map<string, string>,
|
||||
hostAttributes?: Map<string, string>,
|
||||
hostActions?: Map<string, string>,
|
||||
properties?: List<string>,
|
||||
readAttributes?: List<string>,
|
||||
type?: number,
|
||||
callOnDestroy?: boolean,
|
||||
callOnChange?: boolean,
|
||||
callOnCheck?: boolean,
|
||||
callOnInit?: boolean,
|
||||
callOnAllChangesDone?: boolean,
|
||||
changeDetection?: string,
|
||||
exportAs?: string
|
||||
}) {
|
||||
this.id = id;
|
||||
this.selector = selector;
|
||||
this.compileChildren = isPresent(compileChildren) ? compileChildren : true;
|
||||
this.events = events;
|
||||
this.hostListeners = hostListeners;
|
||||
this.hostAttributes = hostAttributes;
|
||||
this.hostProperties = hostProperties;
|
||||
this.hostActions = hostActions;
|
||||
this.properties = properties;
|
||||
this.readAttributes = readAttributes;
|
||||
this.type = type;
|
||||
this.callOnDestroy = callOnDestroy;
|
||||
this.callOnChange = callOnChange;
|
||||
this.callOnCheck = callOnCheck;
|
||||
this.callOnInit = callOnInit;
|
||||
this.callOnAllChangesDone = callOnAllChangesDone;
|
||||
this.changeDetection = changeDetection;
|
||||
this.exportAs = exportAs;
|
||||
}
|
||||
|
||||
static create({id, selector, compileChildren, events, host, properties, readAttributes, type,
|
||||
callOnDestroy, callOnChange, callOnCheck, callOnInit, callOnAllChangesDone,
|
||||
changeDetection, exportAs}: {
|
||||
id?: string,
|
||||
selector?: string,
|
||||
compileChildren?: boolean,
|
||||
events?: List<string>,
|
||||
host?: Map<string, string>,
|
||||
properties?: List<string>,
|
||||
readAttributes?: List<string>,
|
||||
type?: number,
|
||||
callOnDestroy?: boolean,
|
||||
callOnChange?: boolean,
|
||||
callOnCheck?: boolean,
|
||||
callOnInit?: boolean,
|
||||
callOnAllChangesDone?: boolean,
|
||||
changeDetection?: string,
|
||||
exportAs?: string
|
||||
}): RenderDirectiveMetadata {
|
||||
let hostListeners = new Map();
|
||||
let hostProperties = new Map();
|
||||
let hostAttributes = new Map();
|
||||
let hostActions = new Map();
|
||||
|
||||
if (isPresent(host)) {
|
||||
MapWrapper.forEach(host, (value: string, key: string) => {
|
||||
var matches = RegExpWrapper.firstMatch(RenderDirectiveMetadata._hostRegExp, key);
|
||||
if (isBlank(matches)) {
|
||||
hostAttributes.set(key, value);
|
||||
} else if (isPresent(matches[1])) {
|
||||
hostProperties.set(matches[1], value);
|
||||
} else if (isPresent(matches[2])) {
|
||||
hostListeners.set(matches[2], value);
|
||||
} else if (isPresent(matches[3])) {
|
||||
hostActions.set(matches[3], value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new RenderDirectiveMetadata({
|
||||
id: id,
|
||||
selector: selector,
|
||||
compileChildren: compileChildren,
|
||||
events: events,
|
||||
hostListeners: hostListeners,
|
||||
hostProperties: hostProperties,
|
||||
hostAttributes: hostAttributes,
|
||||
hostActions: hostActions,
|
||||
properties: properties,
|
||||
readAttributes: readAttributes,
|
||||
type: type,
|
||||
callOnDestroy: callOnDestroy,
|
||||
callOnChange: callOnChange,
|
||||
callOnCheck: callOnCheck,
|
||||
callOnInit: callOnInit,
|
||||
callOnAllChangesDone: callOnAllChangesDone,
|
||||
changeDetection: changeDetection,
|
||||
exportAs: exportAs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// An opaque reference to a render proto ivew
|
||||
export class RenderProtoViewRef {}
|
||||
|
||||
// An opaque reference to a part of a view
|
||||
export class RenderFragmentRef {}
|
||||
|
||||
// An opaque reference to a view
|
||||
export class RenderViewRef {}
|
||||
|
||||
/**
|
||||
* How the template and styles of a view should be encapsulated.
|
||||
*/
|
||||
export enum ViewEncapsulation {
|
||||
/**
|
||||
* Emulate scoping of styles by preprocessing the style rules
|
||||
* and adding additional attributes to elements. This is the default.
|
||||
*/
|
||||
EMULATED,
|
||||
/**
|
||||
* Uses the native mechanism of the renderer. For the DOM this means creating a ShadowRoot.
|
||||
*/
|
||||
NATIVE,
|
||||
/**
|
||||
* Don't scope the template nor the styles.
|
||||
*/
|
||||
NONE
|
||||
}
|
||||
|
||||
export class ViewDefinition {
|
||||
componentId: string;
|
||||
templateAbsUrl: string;
|
||||
template: string;
|
||||
directives: List<RenderDirectiveMetadata>;
|
||||
styleAbsUrls: List<string>;
|
||||
styles: List<string>;
|
||||
encapsulation: ViewEncapsulation;
|
||||
|
||||
constructor({componentId, templateAbsUrl, template, styleAbsUrls, styles, directives,
|
||||
encapsulation}: {
|
||||
componentId?: string,
|
||||
templateAbsUrl?: string,
|
||||
template?: string,
|
||||
styleAbsUrls?: List<string>,
|
||||
styles?: List<string>,
|
||||
directives?: List<RenderDirectiveMetadata>,
|
||||
encapsulation?: ViewEncapsulation
|
||||
} = {}) {
|
||||
this.componentId = componentId;
|
||||
this.templateAbsUrl = templateAbsUrl;
|
||||
this.template = template;
|
||||
this.styleAbsUrls = styleAbsUrls;
|
||||
this.styles = styles;
|
||||
this.directives = directives;
|
||||
this.encapsulation = isPresent(encapsulation) ? encapsulation : ViewEncapsulation.EMULATED;
|
||||
}
|
||||
}
|
||||
|
||||
export class RenderProtoViewMergeMapping {
|
||||
constructor(public mergedProtoViewRef: RenderProtoViewRef,
|
||||
// Number of fragments in the merged ProtoView.
|
||||
// Fragments are stored in depth first order of nested ProtoViews.
|
||||
public fragmentCount: number,
|
||||
// Mapping from app element index to render element index.
|
||||
// Mappings of nested ProtoViews are in depth first order, with all
|
||||
// indices for one ProtoView in a consecuitve block.
|
||||
public mappedElementIndices: number[],
|
||||
// Number of bound render element.
|
||||
// Note: This could be more than the original ones
|
||||
// as we might have bound a new element for projecting bound text nodes.
|
||||
public mappedElementCount: number,
|
||||
// Mapping from app text index to render text index.
|
||||
// Mappings of nested ProtoViews are in depth first order, with all
|
||||
// indices for one ProtoView in a consecuitve block.
|
||||
public mappedTextIndices: number[],
|
||||
// Mapping from view index to app element index
|
||||
public hostElementIndicesByViewIndex: number[],
|
||||
// Number of contained views by view index
|
||||
public nestedViewCountByViewIndex: number[]) {}
|
||||
}
|
||||
|
||||
export class RenderCompiler {
|
||||
/**
|
||||
* Creats a ProtoViewDto that contains a single nested component with the given componentId.
|
||||
*/
|
||||
compileHost(directiveMetadata: RenderDirectiveMetadata): Promise<ProtoViewDto> { return null; }
|
||||
|
||||
/**
|
||||
* Compiles a single DomProtoView. Non recursive so that
|
||||
* we don't need to serialize all possible components over the wire,
|
||||
* but only the needed ones based on previous calls.
|
||||
*/
|
||||
compile(view: ViewDefinition): Promise<ProtoViewDto> { return null; }
|
||||
|
||||
/**
|
||||
* Merges ProtoViews.
|
||||
* The first entry of the array is the protoview into which all the other entries of the array
|
||||
* should be merged.
|
||||
* If the array contains other arrays, they will be merged before processing the parent array.
|
||||
* The array must contain an entry for every component and embedded ProtoView of the first entry.
|
||||
* @param protoViewRefs List of ProtoViewRefs or nested
|
||||
* @return the merge result
|
||||
*/
|
||||
mergeProtoViewsRecursively(
|
||||
protoViewRefs: List<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class RenderViewWithFragments {
|
||||
constructor(public viewRef: RenderViewRef, public fragmentRefs: RenderFragmentRef[]) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract reference to the element which can be marshaled across web-worker boundary.
|
||||
*
|
||||
* This interface is used by the Renderer API.
|
||||
*/
|
||||
export interface RenderElementRef {
|
||||
/**
|
||||
* Reference to the `RenderViewRef` where the `RenderElementRef` is inside of.
|
||||
*/
|
||||
renderView: RenderViewRef;
|
||||
/**
|
||||
* Index of the element inside the `RenderViewRef`.
|
||||
*
|
||||
* This is used internally by the Angular framework to locate elements.
|
||||
*/
|
||||
renderBoundElementIndex: number;
|
||||
}
|
||||
|
||||
export class Renderer {
|
||||
/**
|
||||
* Creates a root host view that includes the given element.
|
||||
* Note that the fragmentCount needs to be passed in so that we can create a result
|
||||
* synchronously even when dealing with webworkers!
|
||||
*
|
||||
* @param {RenderProtoViewRef} hostProtoViewRef a RenderProtoViewRef of type
|
||||
* ProtoViewDto.HOST_VIEW_TYPE
|
||||
* @param {any} hostElementSelector css selector for the host element (will be queried against the
|
||||
* main document)
|
||||
* @return {RenderViewWithFragments} the created view including fragments
|
||||
*/
|
||||
createRootHostView(hostProtoViewRef: RenderProtoViewRef, fragmentCount: number,
|
||||
hostElementSelector: string): RenderViewWithFragments {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a regular view out of the given ProtoView.
|
||||
* Note that the fragmentCount needs to be passed in so that we can create a result
|
||||
* synchronously even when dealing with webworkers!
|
||||
*/
|
||||
createView(protoViewRef: RenderProtoViewRef, fragmentCount: number): RenderViewWithFragments {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the given view after it has been dehydrated and detached
|
||||
*/
|
||||
destroyView(viewRef: RenderViewRef) {}
|
||||
|
||||
/**
|
||||
* Attaches a fragment after another fragment.
|
||||
*/
|
||||
attachFragmentAfterFragment(previousFragmentRef: RenderFragmentRef,
|
||||
fragmentRef: RenderFragmentRef) {}
|
||||
|
||||
/**
|
||||
* Attaches a fragment after an element.
|
||||
*/
|
||||
attachFragmentAfterElement(elementRef: RenderElementRef, fragmentRef: RenderFragmentRef) {}
|
||||
|
||||
/**
|
||||
* Detaches a fragment.
|
||||
*/
|
||||
detachFragment(fragmentRef: RenderFragmentRef) {}
|
||||
|
||||
/**
|
||||
* Hydrates a view after it has been attached. Hydration/dehydration is used for reusing views
|
||||
* inside of the view pool.
|
||||
*/
|
||||
hydrateView(viewRef: RenderViewRef) {}
|
||||
|
||||
/**
|
||||
* Dehydrates a view after it has been attached. Hydration/dehydration is used for reusing views
|
||||
* inside of the view pool.
|
||||
*/
|
||||
dehydrateView(viewRef: RenderViewRef) {}
|
||||
|
||||
/**
|
||||
* Returns the native element at the given location.
|
||||
* Attention: In a WebWorker scenario, this should always return null!
|
||||
*/
|
||||
getNativeElementSync(location: RenderElementRef): any { return null; }
|
||||
|
||||
/**
|
||||
* Sets a property on an element.
|
||||
*/
|
||||
setElementProperty(location: RenderElementRef, propertyName: string, propertyValue: any) {}
|
||||
|
||||
/**
|
||||
* Sets an attribute on an element.
|
||||
*/
|
||||
setElementAttribute(location: RenderElementRef, attributeName: string, attributeValue: string) {}
|
||||
|
||||
/**
|
||||
* Sets a class on an element.
|
||||
*/
|
||||
setElementClass(location: RenderElementRef, className: string, isAdd: boolean) {}
|
||||
|
||||
/**
|
||||
* Sets a style on an element.
|
||||
*/
|
||||
setElementStyle(location: RenderElementRef, styleName: string, styleValue: string) {}
|
||||
|
||||
/**
|
||||
* Calls a method on an element.
|
||||
*/
|
||||
invokeElementMethod(location: RenderElementRef, methodName: string, args: List<any>) {}
|
||||
|
||||
/**
|
||||
* Sets the value of a text node.
|
||||
*/
|
||||
setText(viewRef: RenderViewRef, textNodeIndex: number, text: string) {}
|
||||
|
||||
/**
|
||||
* Sets the dispatcher for all events of the given view
|
||||
*/
|
||||
setEventDispatcher(viewRef: RenderViewRef, dispatcher: RenderEventDispatcher) {}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A dispatcher for all events happening in a view.
|
||||
*/
|
||||
export interface RenderEventDispatcher {
|
||||
/**
|
||||
* Called when an event was triggered for a on-* attribute on an element.
|
||||
* @param {Map<string, any>} locals Locals to be used to evaluate the
|
||||
* event expressions
|
||||
*/
|
||||
dispatchRenderEvent(elementIndex: number, eventName: string, locals: Map<string, any>);
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import {isBlank} from 'angular2/src/facade/lang';
|
||||
import {List, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {CompileElement} from './compile_element';
|
||||
import {CompileStep} from './compile_step';
|
||||
|
||||
/**
|
||||
* Controls the processing order of elements.
|
||||
* Right now it only allows to add a parent element.
|
||||
*/
|
||||
export class CompileControl {
|
||||
_currentStepIndex: number = 0;
|
||||
_parent: CompileElement = null;
|
||||
_results: any[] = null;
|
||||
_additionalChildren: CompileElement[] = null;
|
||||
_ignoreCurrentElement: boolean;
|
||||
|
||||
constructor(public _steps: List<CompileStep>) {}
|
||||
|
||||
// only public so that it can be used by compile_pipeline
|
||||
internalProcess(results: any[], startStepIndex: number, parent: CompileElement,
|
||||
current: CompileElement): CompileElement[] {
|
||||
this._results = results;
|
||||
var previousStepIndex = this._currentStepIndex;
|
||||
var previousParent = this._parent;
|
||||
|
||||
this._ignoreCurrentElement = false;
|
||||
|
||||
for (var i = startStepIndex; i < this._steps.length && !this._ignoreCurrentElement; i++) {
|
||||
var step = this._steps[i];
|
||||
this._parent = parent;
|
||||
this._currentStepIndex = i;
|
||||
step.processElement(parent, current, this);
|
||||
parent = this._parent;
|
||||
}
|
||||
|
||||
if (!this._ignoreCurrentElement) {
|
||||
results.push(current);
|
||||
}
|
||||
|
||||
this._currentStepIndex = previousStepIndex;
|
||||
this._parent = previousParent;
|
||||
|
||||
var localAdditionalChildren = this._additionalChildren;
|
||||
this._additionalChildren = null;
|
||||
return localAdditionalChildren;
|
||||
}
|
||||
|
||||
addParent(newElement: CompileElement) {
|
||||
this.internalProcess(this._results, this._currentStepIndex + 1, this._parent, newElement);
|
||||
this._parent = newElement;
|
||||
}
|
||||
|
||||
addChild(element: CompileElement) {
|
||||
if (isBlank(this._additionalChildren)) {
|
||||
this._additionalChildren = [];
|
||||
}
|
||||
this._additionalChildren.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignores the current element.
|
||||
*
|
||||
* When a step calls `ignoreCurrentElement`, no further steps are executed on the current
|
||||
* element and no `CompileElement` is added to the result list.
|
||||
*/
|
||||
ignoreCurrentElement() { this._ignoreCurrentElement = true; }
|
||||
}
|
103
modules/angular2/src/core/render/dom/compiler/compile_element.ts
Normal file
103
modules/angular2/src/core/render/dom/compiler/compile_element.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {isBlank, isPresent, Type, StringJoiner, assertionsEnabled} from 'angular2/src/facade/lang';
|
||||
|
||||
import {ProtoViewBuilder, ElementBinderBuilder} from '../view/proto_view_builder';
|
||||
|
||||
/**
|
||||
* Collects all data that is needed to process an element
|
||||
* in the compile process. Fields are filled
|
||||
* by the CompileSteps starting out with the pure HTMLElement.
|
||||
*/
|
||||
export class CompileElement {
|
||||
_attrs: Map<string, string> = null;
|
||||
_classList: List<string> = null;
|
||||
isViewRoot: boolean = false;
|
||||
// inherited down to children if they don't have an own protoView
|
||||
inheritedProtoView: ProtoViewBuilder = null;
|
||||
distanceToInheritedBinder: number = 0;
|
||||
// inherited down to children if they don't have an own elementBinder
|
||||
inheritedElementBinder: ElementBinderBuilder = null;
|
||||
compileChildren: boolean = true;
|
||||
elementDescription: string; // e.g. '<div [title]="foo">' : used to provide context in case of
|
||||
// error
|
||||
|
||||
constructor(public element, compilationUnit: string = '') {
|
||||
// description is calculated here as compilation steps may change the element
|
||||
var tplDesc = assertionsEnabled() ? getElementDescription(element) : null;
|
||||
if (compilationUnit !== '') {
|
||||
this.elementDescription = compilationUnit;
|
||||
if (isPresent(tplDesc)) this.elementDescription += ": " + tplDesc;
|
||||
} else {
|
||||
this.elementDescription = tplDesc;
|
||||
}
|
||||
}
|
||||
|
||||
isBound(): boolean {
|
||||
return isPresent(this.inheritedElementBinder) && this.distanceToInheritedBinder === 0;
|
||||
}
|
||||
|
||||
bindElement(): ElementBinderBuilder {
|
||||
if (!this.isBound()) {
|
||||
var parentBinder = this.inheritedElementBinder;
|
||||
this.inheritedElementBinder =
|
||||
this.inheritedProtoView.bindElement(this.element, this.elementDescription);
|
||||
if (isPresent(parentBinder)) {
|
||||
this.inheritedElementBinder.setParent(parentBinder, this.distanceToInheritedBinder);
|
||||
}
|
||||
this.distanceToInheritedBinder = 0;
|
||||
}
|
||||
return this.inheritedElementBinder;
|
||||
}
|
||||
|
||||
attrs(): Map<string, string> {
|
||||
if (isBlank(this._attrs)) {
|
||||
this._attrs = DOM.attributeMap(this.element);
|
||||
}
|
||||
return this._attrs;
|
||||
}
|
||||
|
||||
classList(): List<string> {
|
||||
if (isBlank(this._classList)) {
|
||||
this._classList = [];
|
||||
var elClassList = DOM.classList(this.element);
|
||||
for (var i = 0; i < elClassList.length; i++) {
|
||||
this._classList.push(elClassList[i]);
|
||||
}
|
||||
}
|
||||
return this._classList;
|
||||
}
|
||||
}
|
||||
|
||||
// return an HTML representation of an element start tag - without its content
|
||||
// this is used to give contextual information in case of errors
|
||||
function getElementDescription(domElement): string {
|
||||
var buf = new StringJoiner();
|
||||
var atts = DOM.attributeMap(domElement);
|
||||
|
||||
buf.add("<");
|
||||
buf.add(DOM.tagName(domElement).toLowerCase());
|
||||
|
||||
// show id and class first to ease element identification
|
||||
addDescriptionAttribute(buf, "id", atts.get("id"));
|
||||
addDescriptionAttribute(buf, "class", atts.get("class"));
|
||||
MapWrapper.forEach(atts, (attValue, attName) => {
|
||||
if (attName !== "id" && attName !== "class") {
|
||||
addDescriptionAttribute(buf, attName, attValue);
|
||||
}
|
||||
});
|
||||
|
||||
buf.add(">");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
|
||||
function addDescriptionAttribute(buffer: StringJoiner, attName: string, attValue) {
|
||||
if (isPresent(attValue)) {
|
||||
if (attValue.length === 0) {
|
||||
buffer.add(' ' + attName);
|
||||
} else {
|
||||
buffer.add(' ' + attName + '="' + attValue + '"');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import {isPresent, isBlank} from 'angular2/src/facade/lang';
|
||||
import {List, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {CompileElement} from './compile_element';
|
||||
import {CompileControl} from './compile_control';
|
||||
import {CompileStep} from './compile_step';
|
||||
import {ProtoViewBuilder} from '../view/proto_view_builder';
|
||||
import {ProtoViewDto, ViewType, ViewDefinition} from '../../api';
|
||||
|
||||
/**
|
||||
* CompilePipeline for executing CompileSteps recursively for
|
||||
* all elements in a template.
|
||||
*/
|
||||
export class CompilePipeline {
|
||||
_control: CompileControl;
|
||||
constructor(public steps: List<CompileStep>) { this._control = new CompileControl(steps); }
|
||||
|
||||
processStyles(styles: string[]): string[] {
|
||||
return styles.map(style => {
|
||||
this.steps.forEach(step => { style = step.processStyle(style); });
|
||||
return style;
|
||||
});
|
||||
}
|
||||
|
||||
processElements(rootElement: Element, protoViewType: ViewType,
|
||||
viewDef: ViewDefinition): CompileElement[] {
|
||||
var results: CompileElement[] = [];
|
||||
var compilationCtxtDescription = viewDef.componentId;
|
||||
var rootCompileElement = new CompileElement(rootElement, compilationCtxtDescription);
|
||||
rootCompileElement.inheritedProtoView =
|
||||
new ProtoViewBuilder(rootElement, protoViewType, viewDef.encapsulation);
|
||||
rootCompileElement.isViewRoot = true;
|
||||
this._processElement(results, null, rootCompileElement, compilationCtxtDescription);
|
||||
return results;
|
||||
}
|
||||
|
||||
_processElement(results: CompileElement[], parent: CompileElement, current: CompileElement,
|
||||
compilationCtxtDescription: string = '') {
|
||||
var additionalChildren = this._control.internalProcess(results, 0, parent, current);
|
||||
|
||||
if (current.compileChildren) {
|
||||
var node = DOM.firstChild(DOM.templateAwareRoot(current.element));
|
||||
while (isPresent(node)) {
|
||||
// compiliation can potentially move the node, so we need to store the
|
||||
// next sibling before recursing.
|
||||
var nextNode = DOM.nextSibling(node);
|
||||
if (DOM.isElementNode(node)) {
|
||||
var childCompileElement = new CompileElement(node, compilationCtxtDescription);
|
||||
childCompileElement.inheritedProtoView = current.inheritedProtoView;
|
||||
childCompileElement.inheritedElementBinder = current.inheritedElementBinder;
|
||||
childCompileElement.distanceToInheritedBinder = current.distanceToInheritedBinder + 1;
|
||||
this._processElement(results, current, childCompileElement);
|
||||
}
|
||||
node = nextNode;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPresent(additionalChildren)) {
|
||||
for (var i = 0; i < additionalChildren.length; i++) {
|
||||
this._processElement(results, current, additionalChildren[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import {CompileElement} from './compile_element';
|
||||
import * as compileControlModule from './compile_control';
|
||||
|
||||
/**
|
||||
* One part of the compile process.
|
||||
* Is guaranteed to be called in depth first order
|
||||
*/
|
||||
export interface CompileStep {
|
||||
processElement(parent: CompileElement, current: CompileElement,
|
||||
control: compileControlModule.CompileControl): void;
|
||||
|
||||
processStyle(style: string): string;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import {List} from 'angular2/src/facade/collection';
|
||||
import {Parser} from 'angular2/src/change_detection/change_detection';
|
||||
import {ViewDefinition} from '../../api';
|
||||
import {CompileStep} from './compile_step';
|
||||
import {PropertyBindingParser} from './property_binding_parser';
|
||||
import {TextInterpolationParser} from './text_interpolation_parser';
|
||||
import {DirectiveParser} from './directive_parser';
|
||||
import {ViewSplitter} from './view_splitter';
|
||||
import {StyleEncapsulator} from './style_encapsulator';
|
||||
|
||||
export class CompileStepFactory {
|
||||
createSteps(view: ViewDefinition): List<CompileStep> { return null; }
|
||||
}
|
||||
|
||||
export class DefaultStepFactory extends CompileStepFactory {
|
||||
private _componentUIDsCache: Map<string, string> = new Map();
|
||||
constructor(private _parser: Parser, private _appId: string) { super(); }
|
||||
|
||||
createSteps(view: ViewDefinition): List<CompileStep> {
|
||||
return [
|
||||
new ViewSplitter(this._parser),
|
||||
new PropertyBindingParser(this._parser),
|
||||
new DirectiveParser(this._parser, view.directives),
|
||||
new TextInterpolationParser(this._parser),
|
||||
new StyleEncapsulator(this._appId, view, this._componentUIDsCache)
|
||||
];
|
||||
}
|
||||
}
|
133
modules/angular2/src/core/render/dom/compiler/compiler.ts
Normal file
133
modules/angular2/src/core/render/dom/compiler/compiler.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import {Injectable} from 'angular2/di';
|
||||
|
||||
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
|
||||
import {BaseException, isPresent, isBlank} from 'angular2/src/facade/lang';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import {
|
||||
ViewDefinition,
|
||||
ProtoViewDto,
|
||||
ViewType,
|
||||
RenderDirectiveMetadata,
|
||||
RenderCompiler,
|
||||
RenderProtoViewRef,
|
||||
RenderProtoViewMergeMapping,
|
||||
ViewEncapsulation
|
||||
} from '../../api';
|
||||
import {CompilePipeline} from './compile_pipeline';
|
||||
import {ViewLoader, TemplateAndStyles} from 'angular2/src/render/dom/compiler/view_loader';
|
||||
import {CompileStepFactory, DefaultStepFactory} from './compile_step_factory';
|
||||
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
|
||||
import {Parser} from 'angular2/src/change_detection/change_detection';
|
||||
import * as pvm from '../view/proto_view_merger';
|
||||
import {CssSelector} from './selector';
|
||||
import {DOCUMENT, APP_ID} from '../dom_tokens';
|
||||
import {Inject} from 'angular2/di';
|
||||
import {SharedStylesHost} from '../view/shared_styles_host';
|
||||
import {prependAll} from '../util';
|
||||
import {TemplateCloner} from '../template_cloner';
|
||||
|
||||
/**
|
||||
* The compiler loads and translates the html templates of components into
|
||||
* nested ProtoViews. To decompose its functionality it uses
|
||||
* the CompilePipeline and the CompileSteps.
|
||||
*/
|
||||
export class DomCompiler extends RenderCompiler {
|
||||
constructor(private _schemaRegistry: ElementSchemaRegistry,
|
||||
private _templateCloner: TemplateCloner, private _stepFactory: CompileStepFactory,
|
||||
private _viewLoader: ViewLoader, private _sharedStylesHost: SharedStylesHost) {
|
||||
super();
|
||||
}
|
||||
|
||||
compile(view: ViewDefinition): Promise<ProtoViewDto> {
|
||||
var tplPromise = this._viewLoader.load(view);
|
||||
return PromiseWrapper.then(
|
||||
tplPromise, (tplAndStyles: TemplateAndStyles) =>
|
||||
this._compileView(view, tplAndStyles, ViewType.COMPONENT),
|
||||
(e) => {
|
||||
throw new BaseException(`Failed to load the template for "${view.componentId}" : ${e}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
compileHost(directiveMetadata: RenderDirectiveMetadata): Promise<ProtoViewDto> {
|
||||
let hostViewDef = new ViewDefinition({
|
||||
componentId: directiveMetadata.id,
|
||||
templateAbsUrl: null, template: null,
|
||||
styles: null,
|
||||
styleAbsUrls: null,
|
||||
directives: [directiveMetadata],
|
||||
encapsulation: ViewEncapsulation.NONE
|
||||
});
|
||||
|
||||
let selector = CssSelector.parse(directiveMetadata.selector)[0];
|
||||
let hostTemplate = selector.getMatchingElementTemplate();
|
||||
let templateAndStyles = new TemplateAndStyles(hostTemplate, []);
|
||||
|
||||
return this._compileView(hostViewDef, templateAndStyles, ViewType.HOST);
|
||||
}
|
||||
|
||||
mergeProtoViewsRecursively(
|
||||
protoViewRefs: List<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping> {
|
||||
return PromiseWrapper.resolve(
|
||||
pvm.mergeProtoViewsRecursively(this._templateCloner, protoViewRefs));
|
||||
}
|
||||
|
||||
_compileView(viewDef: ViewDefinition, templateAndStyles: TemplateAndStyles,
|
||||
protoViewType: ViewType): Promise<ProtoViewDto> {
|
||||
if (viewDef.encapsulation === ViewEncapsulation.EMULATED &&
|
||||
templateAndStyles.styles.length === 0) {
|
||||
viewDef = this._normalizeViewEncapsulationIfThereAreNoStyles(viewDef);
|
||||
}
|
||||
var pipeline = new CompilePipeline(this._stepFactory.createSteps(viewDef));
|
||||
|
||||
var compiledStyles = pipeline.processStyles(templateAndStyles.styles);
|
||||
var compileElements = pipeline.processElements(
|
||||
this._createTemplateElm(templateAndStyles.template), protoViewType, viewDef);
|
||||
if (viewDef.encapsulation === ViewEncapsulation.NATIVE) {
|
||||
prependAll(DOM.content(compileElements[0].element),
|
||||
compiledStyles.map(style => DOM.createStyleElement(style)));
|
||||
} else {
|
||||
this._sharedStylesHost.addStyles(compiledStyles);
|
||||
}
|
||||
|
||||
return PromiseWrapper.resolve(
|
||||
compileElements[0].inheritedProtoView.build(this._schemaRegistry, this._templateCloner));
|
||||
}
|
||||
|
||||
_createTemplateElm(template: string) {
|
||||
var templateElm = DOM.createTemplate(template);
|
||||
var scriptTags = DOM.querySelectorAll(DOM.templateAwareRoot(templateElm), 'script');
|
||||
|
||||
for (var i = 0; i < scriptTags.length; i++) {
|
||||
DOM.remove(scriptTags[i]);
|
||||
}
|
||||
|
||||
return templateElm;
|
||||
}
|
||||
|
||||
_normalizeViewEncapsulationIfThereAreNoStyles(viewDef: ViewDefinition): ViewDefinition {
|
||||
if (viewDef.encapsulation === ViewEncapsulation.EMULATED) {
|
||||
return new ViewDefinition({
|
||||
componentId: viewDef.componentId,
|
||||
templateAbsUrl: viewDef.templateAbsUrl, template: viewDef.template,
|
||||
styleAbsUrls: viewDef.styleAbsUrls,
|
||||
styles: viewDef.styles,
|
||||
directives: viewDef.directives,
|
||||
encapsulation: ViewEncapsulation.NONE
|
||||
});
|
||||
} else {
|
||||
return viewDef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DefaultDomCompiler extends DomCompiler {
|
||||
constructor(schemaRegistry: ElementSchemaRegistry, templateCloner: TemplateCloner, parser: Parser,
|
||||
viewLoader: ViewLoader, sharedStylesHost: SharedStylesHost,
|
||||
@Inject(APP_ID) appId: any) {
|
||||
super(schemaRegistry, templateCloner, new DefaultStepFactory(parser, appId), viewLoader,
|
||||
sharedStylesHost);
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
import {isPresent, isBlank, BaseException, StringWrapper} from 'angular2/src/facade/lang';
|
||||
import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {Parser} from 'angular2/src/change_detection/change_detection';
|
||||
|
||||
import {SelectorMatcher, CssSelector} from 'angular2/src/render/dom/compiler/selector';
|
||||
|
||||
import {CompileStep} from './compile_step';
|
||||
import {CompileElement} from './compile_element';
|
||||
import {CompileControl} from './compile_control';
|
||||
|
||||
import {RenderDirectiveMetadata} from '../../api';
|
||||
import {EventConfig, dashCaseToCamelCase, camelCaseToDashCase} from '../util';
|
||||
import {DirectiveBuilder, ElementBinderBuilder} from '../view/proto_view_builder';
|
||||
|
||||
/**
|
||||
* Parses the directives on a single element. Assumes ViewSplitter has already created
|
||||
* <template> elements for template directives.
|
||||
*/
|
||||
export class DirectiveParser implements CompileStep {
|
||||
_selectorMatcher: SelectorMatcher = new SelectorMatcher();
|
||||
|
||||
constructor(public _parser: Parser, public _directives: List<RenderDirectiveMetadata>) {
|
||||
for (var i = 0; i < _directives.length; i++) {
|
||||
var directive = _directives[i];
|
||||
var selector = CssSelector.parse(directive.selector);
|
||||
this._selectorMatcher.addSelectables(selector, i);
|
||||
}
|
||||
}
|
||||
|
||||
processStyle(style: string): string { return style; }
|
||||
|
||||
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
|
||||
var attrs = current.attrs();
|
||||
var classList = current.classList();
|
||||
var cssSelector = new CssSelector();
|
||||
var foundDirectiveIndices = [];
|
||||
var elementBinder: ElementBinderBuilder = null;
|
||||
|
||||
cssSelector.setElement(DOM.nodeName(current.element));
|
||||
for (var i = 0; i < classList.length; i++) {
|
||||
cssSelector.addClassName(classList[i]);
|
||||
}
|
||||
MapWrapper.forEach(attrs,
|
||||
(attrValue, attrName) => { cssSelector.addAttribute(attrName, attrValue); });
|
||||
|
||||
this._selectorMatcher.match(cssSelector, (selector, directiveIndex) => {
|
||||
var directive = this._directives[directiveIndex];
|
||||
|
||||
elementBinder = current.bindElement();
|
||||
if (directive.type === RenderDirectiveMetadata.COMPONENT_TYPE) {
|
||||
this._ensureHasOnlyOneComponent(elementBinder, current.elementDescription);
|
||||
|
||||
// components need to go first, so it is easier to locate them in the result.
|
||||
ListWrapper.insert(foundDirectiveIndices, 0, directiveIndex);
|
||||
elementBinder.setComponentId(directive.id);
|
||||
} else {
|
||||
foundDirectiveIndices.push(directiveIndex);
|
||||
}
|
||||
});
|
||||
|
||||
ListWrapper.forEach(foundDirectiveIndices, (directiveIndex) => {
|
||||
var dirMetadata = this._directives[directiveIndex];
|
||||
var directiveBinderBuilder = elementBinder.bindDirective(directiveIndex);
|
||||
current.compileChildren = current.compileChildren && dirMetadata.compileChildren;
|
||||
if (isPresent(dirMetadata.properties)) {
|
||||
ListWrapper.forEach(dirMetadata.properties, (bindConfig) => {
|
||||
this._bindDirectiveProperty(bindConfig, current, directiveBinderBuilder);
|
||||
});
|
||||
}
|
||||
if (isPresent(dirMetadata.hostListeners)) {
|
||||
this._sortedKeysForEach(dirMetadata.hostListeners, (action, eventName) => {
|
||||
this._bindDirectiveEvent(eventName, action, current, directiveBinderBuilder);
|
||||
});
|
||||
}
|
||||
if (isPresent(dirMetadata.hostProperties)) {
|
||||
this._sortedKeysForEach(dirMetadata.hostProperties, (expression, hostPropertyName) => {
|
||||
this._bindHostProperty(hostPropertyName, expression, current, directiveBinderBuilder);
|
||||
});
|
||||
}
|
||||
if (isPresent(dirMetadata.hostAttributes)) {
|
||||
this._sortedKeysForEach(dirMetadata.hostAttributes, (hostAttrValue, hostAttrName) => {
|
||||
this._addHostAttribute(hostAttrName, hostAttrValue, current);
|
||||
});
|
||||
}
|
||||
if (isPresent(dirMetadata.readAttributes)) {
|
||||
ListWrapper.forEach(dirMetadata.readAttributes,
|
||||
(attrName) => { elementBinder.readAttribute(attrName); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_sortedKeysForEach(map: Map<string, string>, fn: (value: string, key: string) => void): void {
|
||||
var keys = MapWrapper.keys(map);
|
||||
ListWrapper.sort(keys, (a, b) => {
|
||||
// Ensure a stable sort.
|
||||
var compareVal = StringWrapper.compare(a, b);
|
||||
return compareVal == 0 ? -1 : compareVal;
|
||||
});
|
||||
ListWrapper.forEach(keys, (key) => { fn(MapWrapper.get(map, key), key); });
|
||||
}
|
||||
|
||||
_ensureHasOnlyOneComponent(elementBinder: ElementBinderBuilder, elDescription: string): void {
|
||||
if (isPresent(elementBinder.componentId)) {
|
||||
throw new BaseException(
|
||||
`Only one component directive is allowed per element - check ${elDescription}`);
|
||||
}
|
||||
}
|
||||
|
||||
_bindDirectiveProperty(bindConfig: string, compileElement: CompileElement,
|
||||
directiveBinderBuilder: DirectiveBuilder) {
|
||||
// Name of the property on the directive
|
||||
let dirProperty: string;
|
||||
// Name of the property on the element
|
||||
let elProp: string;
|
||||
let pipes: List<string>;
|
||||
let assignIndex: number = bindConfig.indexOf(':');
|
||||
|
||||
if (assignIndex > -1) {
|
||||
// canonical syntax: `dirProp: elProp | pipe0 | ... | pipeN`
|
||||
dirProperty = StringWrapper.substring(bindConfig, 0, assignIndex).trim();
|
||||
pipes = this._splitBindConfig(StringWrapper.substring(bindConfig, assignIndex + 1));
|
||||
elProp = ListWrapper.removeAt(pipes, 0);
|
||||
} else {
|
||||
// shorthand syntax when the name of the property on the directive and on the element is the
|
||||
// same, ie `property`
|
||||
dirProperty = bindConfig;
|
||||
elProp = bindConfig;
|
||||
pipes = [];
|
||||
}
|
||||
elProp = dashCaseToCamelCase(elProp);
|
||||
var bindingAst = compileElement.bindElement().propertyBindings.get(elProp);
|
||||
if (isBlank(bindingAst)) {
|
||||
var attributeValue = compileElement.attrs().get(camelCaseToDashCase(elProp));
|
||||
if (isPresent(attributeValue)) {
|
||||
bindingAst =
|
||||
this._parser.wrapLiteralPrimitive(attributeValue, compileElement.elementDescription);
|
||||
}
|
||||
}
|
||||
|
||||
// Bindings are optional, so this binding only needs to be set up if an expression is given.
|
||||
if (isPresent(bindingAst)) {
|
||||
directiveBinderBuilder.bindProperty(dirProperty, bindingAst, elProp);
|
||||
}
|
||||
}
|
||||
|
||||
_bindDirectiveEvent(eventName, action, compileElement, directiveBinderBuilder) {
|
||||
var ast = this._parser.parseAction(action, compileElement.elementDescription);
|
||||
var parsedEvent = EventConfig.parse(eventName);
|
||||
var targetName = parsedEvent.isLongForm ? parsedEvent.fieldName : null;
|
||||
directiveBinderBuilder.bindEvent(parsedEvent.eventName, ast, targetName);
|
||||
}
|
||||
|
||||
_bindHostProperty(hostPropertyName, expression, compileElement, directiveBinderBuilder) {
|
||||
var ast = this._parser.parseSimpleBinding(
|
||||
expression, `hostProperties of ${compileElement.elementDescription}`);
|
||||
directiveBinderBuilder.bindHostProperty(hostPropertyName, ast);
|
||||
}
|
||||
|
||||
_addHostAttribute(attrName, attrValue, compileElement) {
|
||||
if (StringWrapper.equals(attrName, 'class')) {
|
||||
ListWrapper.forEach(attrValue.split(' '),
|
||||
(className) => { DOM.addClass(compileElement.element, className); });
|
||||
} else if (!DOM.hasAttribute(compileElement.element, attrName)) {
|
||||
DOM.setAttribute(compileElement.element, attrName, attrValue);
|
||||
}
|
||||
}
|
||||
|
||||
_splitBindConfig(bindConfig: string) {
|
||||
return ListWrapper.map(bindConfig.split('|'), (s) => s.trim());
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
import {isPresent, RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
|
||||
import {MapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {Parser} from 'angular2/src/change_detection/change_detection';
|
||||
|
||||
import {CompileStep} from './compile_step';
|
||||
import {CompileElement} from './compile_element';
|
||||
import {CompileControl} from './compile_control';
|
||||
|
||||
import {dashCaseToCamelCase} from '../util';
|
||||
|
||||
// Group 1 = "bind-"
|
||||
// Group 2 = "var-" or "#"
|
||||
// Group 3 = "on-"
|
||||
// Group 4 = "onbubble-"
|
||||
// Group 5 = "bindon-"
|
||||
// Group 6 = the identifier after "bind-", "var-/#", or "on-"
|
||||
// Group 7 = idenitifer inside [()]
|
||||
// Group 8 = idenitifer inside []
|
||||
// Group 9 = identifier inside ()
|
||||
var BIND_NAME_REGEXP =
|
||||
/^(?:(?:(?:(bind-)|(var-|#)|(on-)|(onbubble-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g;
|
||||
/**
|
||||
* Parses the property bindings on a single element.
|
||||
*/
|
||||
export class PropertyBindingParser implements CompileStep {
|
||||
constructor(private _parser: Parser) {}
|
||||
|
||||
processStyle(style: string): string { return style; }
|
||||
|
||||
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
|
||||
var attrs = current.attrs();
|
||||
var newAttrs = new Map();
|
||||
|
||||
MapWrapper.forEach(attrs, (attrValue, attrName) => {
|
||||
|
||||
attrName = this._normalizeAttributeName(attrName);
|
||||
|
||||
var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName);
|
||||
if (isPresent(bindParts)) {
|
||||
if (isPresent(bindParts[1])) { // match: bind-prop
|
||||
this._bindProperty(bindParts[6], attrValue, current, newAttrs);
|
||||
|
||||
} else if (isPresent(
|
||||
bindParts[2])) { // match: var-name / var-name="iden" / #name / #name="iden"
|
||||
var identifier = bindParts[6];
|
||||
var value = attrValue == '' ? '\$implicit' : attrValue;
|
||||
this._bindVariable(identifier, value, current, newAttrs);
|
||||
|
||||
} else if (isPresent(bindParts[3])) { // match: on-event
|
||||
this._bindEvent(bindParts[6], attrValue, current, newAttrs);
|
||||
|
||||
} else if (isPresent(bindParts[4])) { // match: onbubble-event
|
||||
this._bindEvent('^' + bindParts[6], attrValue, current, newAttrs);
|
||||
|
||||
} else if (isPresent(bindParts[5])) { // match: bindon-prop
|
||||
this._bindProperty(bindParts[6], attrValue, current, newAttrs);
|
||||
this._bindAssignmentEvent(bindParts[6], attrValue, current, newAttrs);
|
||||
|
||||
} else if (isPresent(bindParts[7])) { // match: [(expr)]
|
||||
this._bindProperty(bindParts[7], attrValue, current, newAttrs);
|
||||
this._bindAssignmentEvent(bindParts[7], attrValue, current, newAttrs);
|
||||
|
||||
} else if (isPresent(bindParts[8])) { // match: [expr]
|
||||
this._bindProperty(bindParts[8], attrValue, current, newAttrs);
|
||||
|
||||
} else if (isPresent(bindParts[9])) { // match: (event)
|
||||
this._bindEvent(bindParts[9], attrValue, current, newAttrs);
|
||||
}
|
||||
} else {
|
||||
var expr = this._parser.parseInterpolation(attrValue, current.elementDescription);
|
||||
if (isPresent(expr)) {
|
||||
this._bindPropertyAst(attrName, expr, current, newAttrs);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MapWrapper.forEach(newAttrs, (attrValue, attrName) => { attrs.set(attrName, attrValue); });
|
||||
}
|
||||
|
||||
_normalizeAttributeName(attrName: string): string {
|
||||
return StringWrapper.startsWith(attrName, 'data-') ? StringWrapper.substring(attrName, 5) :
|
||||
attrName;
|
||||
}
|
||||
|
||||
_bindVariable(identifier, value, current: CompileElement, newAttrs: Map<any, any>) {
|
||||
current.bindElement().bindVariable(dashCaseToCamelCase(identifier), value);
|
||||
newAttrs.set(identifier, value);
|
||||
}
|
||||
|
||||
_bindProperty(name, expression, current: CompileElement, newAttrs) {
|
||||
this._bindPropertyAst(name, this._parser.parseBinding(expression, current.elementDescription),
|
||||
current, newAttrs);
|
||||
}
|
||||
|
||||
_bindPropertyAst(name, ast, current: CompileElement, newAttrs: Map<any, any>) {
|
||||
var binder = current.bindElement();
|
||||
binder.bindProperty(dashCaseToCamelCase(name), ast);
|
||||
newAttrs.set(name, ast.source);
|
||||
}
|
||||
|
||||
_bindAssignmentEvent(name, expression, current: CompileElement, newAttrs) {
|
||||
this._bindEvent(name, `${expression}=$event`, current, newAttrs);
|
||||
}
|
||||
|
||||
_bindEvent(name, expression, current: CompileElement, newAttrs) {
|
||||
current.bindElement().bindEvent(
|
||||
dashCaseToCamelCase(name),
|
||||
this._parser.parseAction(expression, current.elementDescription));
|
||||
// Don't detect directives for event names for now,
|
||||
// so don't add the event name to the CompileElement.attrs
|
||||
}
|
||||
}
|
387
modules/angular2/src/core/render/dom/compiler/selector.ts
Normal file
387
modules/angular2/src/core/render/dom/compiler/selector.ts
Normal file
@ -0,0 +1,387 @@
|
||||
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
isPresent,
|
||||
isBlank,
|
||||
RegExpWrapper,
|
||||
RegExpMatcherWrapper,
|
||||
StringWrapper,
|
||||
BaseException
|
||||
} from 'angular2/src/facade/lang';
|
||||
|
||||
const _EMPTY_ATTR_VALUE = '';
|
||||
|
||||
// TODO: Can't use `const` here as
|
||||
// in Dart this is not transpiled into `final` yet...
|
||||
var _SELECTOR_REGEXP = RegExpWrapper.create(
|
||||
'(\\:not\\()|' + //":not("
|
||||
'([-\\w]+)|' + // "tag"
|
||||
'(?:\\.([-\\w]+))|' + // ".class"
|
||||
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]" or "[name*=value]"
|
||||
'(\\))|' + // ")"
|
||||
'(\\s*,\\s*)'); // ","
|
||||
|
||||
/**
|
||||
* A css selector contains an element name,
|
||||
* css classes and attribute/value pairs with the purpose
|
||||
* of selecting subsets out of them.
|
||||
*/
|
||||
export class CssSelector {
|
||||
element: string = null;
|
||||
classNames: string[] = [];
|
||||
attrs: string[] = [];
|
||||
notSelectors: CssSelector[] = [];
|
||||
|
||||
static parse(selector: string): CssSelector[] {
|
||||
var results: CssSelector[] = [];
|
||||
var _addResult = (res: CssSelector[], cssSel) => {
|
||||
if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) &&
|
||||
ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) {
|
||||
cssSel.element = "*";
|
||||
}
|
||||
res.push(cssSel);
|
||||
};
|
||||
var cssSelector = new CssSelector();
|
||||
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
|
||||
var match;
|
||||
var current = cssSelector;
|
||||
var inNot = false;
|
||||
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
|
||||
if (isPresent(match[1])) {
|
||||
if (inNot) {
|
||||
throw new BaseException('Nesting :not is not allowed in a selector');
|
||||
}
|
||||
inNot = true;
|
||||
current = new CssSelector();
|
||||
cssSelector.notSelectors.push(current);
|
||||
}
|
||||
if (isPresent(match[2])) {
|
||||
current.setElement(match[2]);
|
||||
}
|
||||
if (isPresent(match[3])) {
|
||||
current.addClassName(match[3]);
|
||||
}
|
||||
if (isPresent(match[4])) {
|
||||
current.addAttribute(match[4], match[5]);
|
||||
}
|
||||
if (isPresent(match[6])) {
|
||||
inNot = false;
|
||||
current = cssSelector;
|
||||
}
|
||||
if (isPresent(match[7])) {
|
||||
if (inNot) {
|
||||
throw new BaseException('Multiple selectors in :not are not supported');
|
||||
}
|
||||
_addResult(results, cssSelector);
|
||||
cssSelector = current = new CssSelector();
|
||||
}
|
||||
}
|
||||
_addResult(results, cssSelector);
|
||||
return results;
|
||||
}
|
||||
|
||||
isElementSelector(): boolean {
|
||||
return isPresent(this.element) && ListWrapper.isEmpty(this.classNames) &&
|
||||
ListWrapper.isEmpty(this.attrs) && this.notSelectors.length === 0;
|
||||
}
|
||||
|
||||
setElement(element: string = null) {
|
||||
if (isPresent(element)) {
|
||||
element = element.toLowerCase();
|
||||
}
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
/** Gets a template string for an element that matches the selector. */
|
||||
getMatchingElementTemplate(): string {
|
||||
let tagName = isPresent(this.element) ? this.element : 'div';
|
||||
let classAttr = this.classNames.length > 0 ? ` class="${this.classNames.join(' ')}"` : '';
|
||||
|
||||
let attrs = '';
|
||||
for (let i = 0; i < this.attrs.length; i += 2) {
|
||||
let attrName = this.attrs[i];
|
||||
let attrValue = this.attrs[i + 1] !== '' ? `="${this.attrs[i + 1]}"` : '';
|
||||
attrs += ` ${attrName}${attrValue}`;
|
||||
}
|
||||
|
||||
return `<${tagName}${classAttr}${attrs}></${tagName}>`;
|
||||
}
|
||||
|
||||
addAttribute(name: string, value: string = _EMPTY_ATTR_VALUE) {
|
||||
this.attrs.push(name.toLowerCase());
|
||||
if (isPresent(value)) {
|
||||
value = value.toLowerCase();
|
||||
} else {
|
||||
value = _EMPTY_ATTR_VALUE;
|
||||
}
|
||||
this.attrs.push(value);
|
||||
}
|
||||
|
||||
addClassName(name: string) { this.classNames.push(name.toLowerCase()); }
|
||||
|
||||
toString(): string {
|
||||
var res = '';
|
||||
if (isPresent(this.element)) {
|
||||
res += this.element;
|
||||
}
|
||||
if (isPresent(this.classNames)) {
|
||||
for (var i = 0; i < this.classNames.length; i++) {
|
||||
res += '.' + this.classNames[i];
|
||||
}
|
||||
}
|
||||
if (isPresent(this.attrs)) {
|
||||
for (var i = 0; i < this.attrs.length;) {
|
||||
var attrName = this.attrs[i++];
|
||||
var attrValue = this.attrs[i++];
|
||||
res += '[' + attrName;
|
||||
if (attrValue.length > 0) {
|
||||
res += '=' + attrValue;
|
||||
}
|
||||
res += ']';
|
||||
}
|
||||
}
|
||||
ListWrapper.forEach(this.notSelectors,
|
||||
(notSelector) => { res += ":not(" + notSelector.toString() + ")"; });
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a list of CssSelectors and allows to calculate which ones
|
||||
* are contained in a given CssSelector.
|
||||
*/
|
||||
export class SelectorMatcher {
|
||||
static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher {
|
||||
var notMatcher = new SelectorMatcher();
|
||||
notMatcher.addSelectables(notSelectors, null);
|
||||
return notMatcher;
|
||||
}
|
||||
|
||||
private _elementMap: Map<string, SelectorContext[]> = new Map();
|
||||
private _elementPartialMap: Map<string, SelectorMatcher> = new Map();
|
||||
private _classMap: Map<string, SelectorContext[]> = new Map();
|
||||
private _classPartialMap: Map<string, SelectorMatcher> = new Map();
|
||||
private _attrValueMap: Map<string, Map<string, SelectorContext[]>> = new Map();
|
||||
private _attrValuePartialMap: Map<string, Map<string, SelectorMatcher>> = new Map();
|
||||
private _listContexts: SelectorListContext[] = [];
|
||||
|
||||
addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) {
|
||||
var listContext = null;
|
||||
if (cssSelectors.length > 1) {
|
||||
listContext = new SelectorListContext(cssSelectors);
|
||||
this._listContexts.push(listContext);
|
||||
}
|
||||
for (var i = 0; i < cssSelectors.length; i++) {
|
||||
this._addSelectable(cssSelectors[i], callbackCtxt, listContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an object that can be found later on by calling `match`.
|
||||
* @param cssSelector A css selector
|
||||
* @param callbackCtxt An opaque object that will be given to the callback of the `match` function
|
||||
*/
|
||||
private _addSelectable(cssSelector: CssSelector, callbackCtxt: any,
|
||||
listContext: SelectorListContext) {
|
||||
var matcher = this;
|
||||
var element = cssSelector.element;
|
||||
var classNames = cssSelector.classNames;
|
||||
var attrs = cssSelector.attrs;
|
||||
var selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
|
||||
|
||||
if (isPresent(element)) {
|
||||
var isTerminal = attrs.length === 0 && classNames.length === 0;
|
||||
if (isTerminal) {
|
||||
this._addTerminal(matcher._elementMap, element, selectable);
|
||||
} else {
|
||||
matcher = this._addPartial(matcher._elementPartialMap, element);
|
||||
}
|
||||
}
|
||||
|
||||
if (isPresent(classNames)) {
|
||||
for (var index = 0; index < classNames.length; index++) {
|
||||
var isTerminal = attrs.length === 0 && index === classNames.length - 1;
|
||||
var className = classNames[index];
|
||||
if (isTerminal) {
|
||||
this._addTerminal(matcher._classMap, className, selectable);
|
||||
} else {
|
||||
matcher = this._addPartial(matcher._classPartialMap, className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPresent(attrs)) {
|
||||
for (var index = 0; index < attrs.length;) {
|
||||
var isTerminal = index === attrs.length - 2;
|
||||
var attrName = attrs[index++];
|
||||
var attrValue = attrs[index++];
|
||||
if (isTerminal) {
|
||||
var terminalMap = matcher._attrValueMap;
|
||||
var terminalValuesMap = terminalMap.get(attrName);
|
||||
if (isBlank(terminalValuesMap)) {
|
||||
terminalValuesMap = new Map();
|
||||
terminalMap.set(attrName, terminalValuesMap);
|
||||
}
|
||||
this._addTerminal(terminalValuesMap, attrValue, selectable);
|
||||
} else {
|
||||
var parttialMap = matcher._attrValuePartialMap;
|
||||
var partialValuesMap = parttialMap.get(attrName);
|
||||
if (isBlank(partialValuesMap)) {
|
||||
partialValuesMap = new Map();
|
||||
parttialMap.set(attrName, partialValuesMap);
|
||||
}
|
||||
matcher = this._addPartial(partialValuesMap, attrValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _addTerminal(map: Map<string, SelectorContext[]>, name: string,
|
||||
selectable: SelectorContext) {
|
||||
var terminalList = map.get(name);
|
||||
if (isBlank(terminalList)) {
|
||||
terminalList = [];
|
||||
map.set(name, terminalList);
|
||||
}
|
||||
terminalList.push(selectable);
|
||||
}
|
||||
|
||||
private _addPartial(map: Map<string, SelectorMatcher>, name: string): SelectorMatcher {
|
||||
var matcher = map.get(name);
|
||||
if (isBlank(matcher)) {
|
||||
matcher = new SelectorMatcher();
|
||||
map.set(name, matcher);
|
||||
}
|
||||
return matcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the objects that have been added via `addSelectable`
|
||||
* whose css selector is contained in the given css selector.
|
||||
* @param cssSelector A css selector
|
||||
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
|
||||
* @return boolean true if a match was found
|
||||
*/
|
||||
match(cssSelector: CssSelector, matchedCallback: (CssSelector, any) => void): boolean {
|
||||
var result = false;
|
||||
var element = cssSelector.element;
|
||||
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;
|
||||
|
||||
if (isPresent(classNames)) {
|
||||
for (var index = 0; index < classNames.length; index++) {
|
||||
var className = classNames[index];
|
||||
result =
|
||||
this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
|
||||
result =
|
||||
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) ||
|
||||
result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPresent(attrs)) {
|
||||
for (var index = 0; index < attrs.length;) {
|
||||
var attrName = attrs[index++];
|
||||
var attrValue = attrs[index++];
|
||||
|
||||
var terminalValuesMap = this._attrValueMap.get(attrName);
|
||||
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
|
||||
result = this._matchTerminal(terminalValuesMap, _EMPTY_ATTR_VALUE, cssSelector,
|
||||
matchedCallback) ||
|
||||
result;
|
||||
}
|
||||
result = this._matchTerminal(terminalValuesMap, attrValue, cssSelector, matchedCallback) ||
|
||||
result;
|
||||
|
||||
var partialValuesMap = this._attrValuePartialMap.get(attrName);
|
||||
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
|
||||
result = this._matchPartial(partialValuesMap, _EMPTY_ATTR_VALUE, cssSelector,
|
||||
matchedCallback) ||
|
||||
result;
|
||||
}
|
||||
result =
|
||||
this._matchPartial(partialValuesMap, attrValue, cssSelector, matchedCallback) || result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_matchTerminal(map: Map<string, SelectorContext[]>, name, cssSelector: CssSelector,
|
||||
matchedCallback: (CssSelector, any) => void): boolean {
|
||||
if (isBlank(map) || isBlank(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var selectables = map.get(name);
|
||||
var starSelectables = map.get("*");
|
||||
if (isPresent(starSelectables)) {
|
||||
selectables = ListWrapper.concat(selectables, starSelectables);
|
||||
}
|
||||
if (isBlank(selectables)) {
|
||||
return false;
|
||||
}
|
||||
var selectable;
|
||||
var result = false;
|
||||
for (var index = 0; index < selectables.length; index++) {
|
||||
selectable = selectables[index];
|
||||
result = selectable.finalize(cssSelector, matchedCallback) || result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_matchPartial(map: Map<string, SelectorMatcher>, name, cssSelector: CssSelector,
|
||||
matchedCallback /*: (CssSelector, any) => void*/): boolean {
|
||||
if (isBlank(map) || isBlank(name)) {
|
||||
return false;
|
||||
}
|
||||
var nestedSelector = map.get(name);
|
||||
if (isBlank(nestedSelector)) {
|
||||
return false;
|
||||
}
|
||||
// TODO(perf): get rid of recursion and measure again
|
||||
// TODO(perf): don't pass the whole selector into the recursion,
|
||||
// but only the not processed parts
|
||||
return nestedSelector.match(cssSelector, matchedCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class SelectorListContext {
|
||||
alreadyMatched: boolean = false;
|
||||
|
||||
constructor(public selectors: CssSelector[]) {}
|
||||
}
|
||||
|
||||
// Store context to pass back selector and context when a selector is matched
|
||||
export class SelectorContext {
|
||||
notSelectors: CssSelector[];
|
||||
|
||||
constructor(public selector: CssSelector, public cbContext: any,
|
||||
public listContext: SelectorListContext) {
|
||||
this.notSelectors = selector.notSelectors;
|
||||
}
|
||||
|
||||
finalize(cssSelector: CssSelector, callback: (CssSelector, any) => void): boolean {
|
||||
var result = true;
|
||||
if (this.notSelectors.length > 0 &&
|
||||
(isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
|
||||
var notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
|
||||
result = !notMatcher.match(cssSelector, null);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
530
modules/angular2/src/core/render/dom/compiler/shadow_css.ts
Normal file
530
modules/angular2/src/core/render/dom/compiler/shadow_css.ts
Normal file
@ -0,0 +1,530 @@
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {List, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
StringWrapper,
|
||||
RegExp,
|
||||
RegExpWrapper,
|
||||
RegExpMatcherWrapper,
|
||||
isPresent,
|
||||
isBlank,
|
||||
BaseException
|
||||
} from 'angular2/src/facade/lang';
|
||||
|
||||
/**
|
||||
* This file is a port of shadowCSS from webcomponents.js to TypeScript.
|
||||
*
|
||||
* Please make sure to keep to edits in sync with the source file.
|
||||
*
|
||||
* Source:
|
||||
* https://github.com/webcomponents/webcomponentsjs/blob/4efecd7e0e/src/ShadowCSS/ShadowCSS.js
|
||||
*
|
||||
* The original file level comment is reproduced below
|
||||
*/
|
||||
|
||||
/*
|
||||
This is a limited shim for ShadowDOM css styling.
|
||||
https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles
|
||||
|
||||
The intention here is to support only the styling features which can be
|
||||
relatively simply implemented. The goal is to allow users to avoid the
|
||||
most obvious pitfalls and do so without compromising performance significantly.
|
||||
For ShadowDOM styling that's not covered here, a set of best practices
|
||||
can be provided that should allow users to accomplish more complex styling.
|
||||
|
||||
The following is a list of specific ShadowDOM styling features and a brief
|
||||
discussion of the approach used to shim.
|
||||
|
||||
Shimmed features:
|
||||
|
||||
* :host, :host-context: ShadowDOM allows styling of the shadowRoot's host
|
||||
element using the :host rule. To shim this feature, the :host styles are
|
||||
reformatted and prefixed with a given scope name and promoted to a
|
||||
document level stylesheet.
|
||||
For example, given a scope name of .foo, a rule like this:
|
||||
|
||||
:host {
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
|
||||
becomes:
|
||||
|
||||
.foo {
|
||||
background: red;
|
||||
}
|
||||
|
||||
* encapsultion: Styles defined within ShadowDOM, apply only to
|
||||
dom inside the ShadowDOM. Polymer uses one of two techniques to imlement
|
||||
this feature.
|
||||
|
||||
By default, rules are prefixed with the host element tag name
|
||||
as a descendant selector. This ensures styling does not leak out of the 'top'
|
||||
of the element's ShadowDOM. For example,
|
||||
|
||||
div {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
becomes:
|
||||
|
||||
x-foo div {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
becomes:
|
||||
|
||||
|
||||
Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then
|
||||
selectors are scoped by adding an attribute selector suffix to each
|
||||
simple selector that contains the host element tag name. Each element
|
||||
in the element's ShadowDOM template is also given the scope attribute.
|
||||
Thus, these rules match only elements that have the scope attribute.
|
||||
For example, given a scope name of x-foo, a rule like this:
|
||||
|
||||
div {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
becomes:
|
||||
|
||||
div[x-foo] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
Note that elements that are dynamically added to a scope must have the scope
|
||||
selector added to them manually.
|
||||
|
||||
* upper/lower bound encapsulation: Styles which are defined outside a
|
||||
shadowRoot should not cross the ShadowDOM boundary and should not apply
|
||||
inside a shadowRoot.
|
||||
|
||||
This styling behavior is not emulated. Some possible ways to do this that
|
||||
were rejected due to complexity and/or performance concerns include: (1) reset
|
||||
every possible property for every possible selector for a given scope name;
|
||||
(2) re-implement css in javascript.
|
||||
|
||||
As an alternative, users should make sure to use selectors
|
||||
specific to the scope in which they are working.
|
||||
|
||||
* ::distributed: This behavior is not emulated. It's often not necessary
|
||||
to style the contents of a specific insertion point and instead, descendants
|
||||
of the host element can be styled selectively. Users can also create an
|
||||
extra node around an insertion point and style that node's contents
|
||||
via descendent selectors. For example, with a shadowRoot like this:
|
||||
|
||||
<style>
|
||||
::content(div) {
|
||||
background: red;
|
||||
}
|
||||
</style>
|
||||
<content></content>
|
||||
|
||||
could become:
|
||||
|
||||
<style>
|
||||
/ *@polyfill .content-container div * /
|
||||
::content(div) {
|
||||
background: red;
|
||||
}
|
||||
</style>
|
||||
<div class="content-container">
|
||||
<content></content>
|
||||
</div>
|
||||
|
||||
Note the use of @polyfill in the comment above a ShadowDOM specific style
|
||||
declaration. This is a directive to the styling shim to use the selector
|
||||
in comments in lieu of the next selector when running under polyfill.
|
||||
*/
|
||||
|
||||
export class ShadowCss {
|
||||
strictStyling: boolean = true;
|
||||
|
||||
constructor() {}
|
||||
|
||||
/*
|
||||
* Shim a style element with the given selector. Returns cssText that can
|
||||
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
|
||||
*/
|
||||
shimStyle(style: string, selector: string, hostSelector: string = ''): string {
|
||||
var cssText = DOM.getText(style);
|
||||
return this.shimCssText(cssText, selector, hostSelector);
|
||||
}
|
||||
|
||||
/*
|
||||
* Shim some cssText with the given selector. Returns cssText that can
|
||||
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
|
||||
*
|
||||
* When strictStyling is true:
|
||||
* - selector is the attribute added to all elements inside the host,
|
||||
* - hostSelector is the attribute added to the host itself.
|
||||
*/
|
||||
shimCssText(cssText: string, selector: string, hostSelector: string = ''): string {
|
||||
cssText = this._insertDirectives(cssText);
|
||||
return this._scopeCssText(cssText, selector, hostSelector);
|
||||
}
|
||||
|
||||
_insertDirectives(cssText: string): string {
|
||||
cssText = this._insertPolyfillDirectivesInCssText(cssText);
|
||||
return this._insertPolyfillRulesInCssText(cssText);
|
||||
}
|
||||
|
||||
/*
|
||||
* Process styles to convert native ShadowDOM rules that will trip
|
||||
* up the css parser; we rely on decorating the stylesheet with inert rules.
|
||||
*
|
||||
* For example, we convert this rule:
|
||||
*
|
||||
* polyfill-next-selector { content: ':host menu-item'; }
|
||||
* ::content menu-item {
|
||||
*
|
||||
* to this:
|
||||
*
|
||||
* scopeName menu-item {
|
||||
*
|
||||
**/
|
||||
_insertPolyfillDirectivesInCssText(cssText: string): string {
|
||||
// Difference with webcomponents.js: does not handle comments
|
||||
return StringWrapper.replaceAllMapped(cssText, _cssContentNextSelectorRe,
|
||||
function(m) { return m[1] + '{'; });
|
||||
}
|
||||
|
||||
/*
|
||||
* Process styles to add rules which will only apply under the polyfill
|
||||
*
|
||||
* For example, we convert this rule:
|
||||
*
|
||||
* polyfill-rule {
|
||||
* content: ':host menu-item';
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* to this:
|
||||
*
|
||||
* scopeName menu-item {...}
|
||||
*
|
||||
**/
|
||||
_insertPolyfillRulesInCssText(cssText: string): string {
|
||||
// Difference with webcomponents.js: does not handle comments
|
||||
return StringWrapper.replaceAllMapped(cssText, _cssContentRuleRe, function(m) {
|
||||
var rule = m[0];
|
||||
rule = StringWrapper.replace(rule, m[1], '');
|
||||
rule = StringWrapper.replace(rule, m[2], '');
|
||||
return m[3] + rule;
|
||||
});
|
||||
}
|
||||
|
||||
/* Ensure styles are scoped. Pseudo-scoping takes a rule like:
|
||||
*
|
||||
* .foo {... }
|
||||
*
|
||||
* and converts this to
|
||||
*
|
||||
* scopeName .foo { ... }
|
||||
*/
|
||||
_scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string {
|
||||
var unscoped = this._extractUnscopedRulesFromCssText(cssText);
|
||||
cssText = this._insertPolyfillHostInCssText(cssText);
|
||||
cssText = this._convertColonHost(cssText);
|
||||
cssText = this._convertColonHostContext(cssText);
|
||||
cssText = this._convertShadowDOMSelectors(cssText);
|
||||
if (isPresent(scopeSelector)) {
|
||||
_withCssRules(cssText,
|
||||
(rules) => { cssText = this._scopeRules(rules, scopeSelector, hostSelector); });
|
||||
}
|
||||
cssText = cssText + '\n' + unscoped;
|
||||
return cssText.trim();
|
||||
}
|
||||
|
||||
/*
|
||||
* Process styles to add rules which will only apply under the polyfill
|
||||
* and do not process via CSSOM. (CSSOM is destructive to rules on rare
|
||||
* occasions, e.g. -webkit-calc on Safari.)
|
||||
* For example, we convert this rule:
|
||||
*
|
||||
* @polyfill-unscoped-rule {
|
||||
* content: 'menu-item';
|
||||
* ... }
|
||||
*
|
||||
* to this:
|
||||
*
|
||||
* menu-item {...}
|
||||
*
|
||||
**/
|
||||
_extractUnscopedRulesFromCssText(cssText: string): string {
|
||||
// Difference with webcomponents.js: does not handle comments
|
||||
var r = '', m;
|
||||
var matcher = RegExpWrapper.matcher(_cssContentUnscopedRuleRe, cssText);
|
||||
while (isPresent(m = RegExpMatcherWrapper.next(matcher))) {
|
||||
var rule = m[0];
|
||||
rule = StringWrapper.replace(rule, m[2], '');
|
||||
rule = StringWrapper.replace(rule, m[1], m[3]);
|
||||
r += rule + '\n\n';
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/*
|
||||
* convert a rule like :host(.foo) > .bar { }
|
||||
*
|
||||
* to
|
||||
*
|
||||
* scopeName.foo > .bar
|
||||
*/
|
||||
_convertColonHost(cssText: string): string {
|
||||
return this._convertColonRule(cssText, _cssColonHostRe, this._colonHostPartReplacer);
|
||||
}
|
||||
|
||||
/*
|
||||
* convert a rule like :host-context(.foo) > .bar { }
|
||||
*
|
||||
* to
|
||||
*
|
||||
* scopeName.foo > .bar, .foo scopeName > .bar { }
|
||||
*
|
||||
* and
|
||||
*
|
||||
* :host-context(.foo:host) .bar { ... }
|
||||
*
|
||||
* to
|
||||
*
|
||||
* scopeName.foo .bar { ... }
|
||||
*/
|
||||
_convertColonHostContext(cssText: string): string {
|
||||
return this._convertColonRule(cssText, _cssColonHostContextRe,
|
||||
this._colonHostContextPartReplacer);
|
||||
}
|
||||
|
||||
_convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
|
||||
// p1 = :host, p2 = contents of (), p3 rest of rule
|
||||
return StringWrapper.replaceAllMapped(cssText, regExp, function(m) {
|
||||
if (isPresent(m[2])) {
|
||||
var parts = m[2].split(','), r = [];
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i];
|
||||
if (isBlank(p)) break;
|
||||
p = p.trim();
|
||||
r.push(partReplacer(_polyfillHostNoCombinator, p, m[3]));
|
||||
}
|
||||
return r.join(',');
|
||||
} else {
|
||||
return _polyfillHostNoCombinator + m[3];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_colonHostContextPartReplacer(host: string, part: string, suffix: string): string {
|
||||
if (StringWrapper.contains(part, _polyfillHost)) {
|
||||
return this._colonHostPartReplacer(host, part, suffix);
|
||||
} else {
|
||||
return host + part + suffix + ', ' + part + ' ' + host + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
_colonHostPartReplacer(host: string, part: string, suffix: string): string {
|
||||
return host + StringWrapper.replace(part, _polyfillHost, '') + suffix;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert combinators like ::shadow and pseudo-elements like ::content
|
||||
* by replacing with space.
|
||||
*/
|
||||
_convertShadowDOMSelectors(cssText: string): string {
|
||||
for (var i = 0; i < _shadowDOMSelectorsRe.length; i++) {
|
||||
cssText = StringWrapper.replaceAll(cssText, _shadowDOMSelectorsRe[i], ' ');
|
||||
}
|
||||
return cssText;
|
||||
}
|
||||
|
||||
// change a selector like 'div' to 'name div'
|
||||
_scopeRules(cssRules, scopeSelector: string, hostSelector: string): string {
|
||||
var cssText = '';
|
||||
if (isPresent(cssRules)) {
|
||||
for (var i = 0; i < cssRules.length; i++) {
|
||||
var rule = cssRules[i];
|
||||
if (DOM.isStyleRule(rule) || DOM.isPageRule(rule)) {
|
||||
cssText += this._scopeSelector(rule.selectorText, scopeSelector, hostSelector,
|
||||
this.strictStyling) +
|
||||
' {\n';
|
||||
cssText += this._propertiesFromRule(rule) + '\n}\n\n';
|
||||
} else if (DOM.isMediaRule(rule)) {
|
||||
cssText += '@media ' + rule.media.mediaText + ' {\n';
|
||||
cssText += this._scopeRules(rule.cssRules, scopeSelector, hostSelector);
|
||||
cssText += '\n}\n\n';
|
||||
} else {
|
||||
// KEYFRAMES_RULE in IE throws when we query cssText
|
||||
// when it contains a -webkit- property.
|
||||
// if this happens, we fallback to constructing the rule
|
||||
// from the CSSRuleSet
|
||||
// https://connect.microsoft.com/IE/feedbackdetail/view/955703/accessing-csstext-of-a-keyframe-rule-that-contains-a-webkit-property-via-cssom-generates-exception
|
||||
try {
|
||||
if (isPresent(rule.cssText)) {
|
||||
cssText += rule.cssText + '\n\n';
|
||||
}
|
||||
} catch (x) {
|
||||
if (DOM.isKeyframesRule(rule) && isPresent(rule.cssRules)) {
|
||||
cssText += this._ieSafeCssTextFromKeyFrameRule(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cssText;
|
||||
}
|
||||
|
||||
_ieSafeCssTextFromKeyFrameRule(rule): string {
|
||||
var cssText = '@keyframes ' + rule.name + ' {';
|
||||
for (var i = 0; i < rule.cssRules.length; i++) {
|
||||
var r = rule.cssRules[i];
|
||||
cssText += ' ' + r.keyText + ' {' + r.style.cssText + '}';
|
||||
}
|
||||
cssText += ' }';
|
||||
return cssText;
|
||||
}
|
||||
|
||||
_scopeSelector(selector: string, scopeSelector: string, hostSelector: string,
|
||||
strict: boolean): string {
|
||||
var r = [], parts = selector.split(',');
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i];
|
||||
p = p.trim();
|
||||
if (this._selectorNeedsScoping(p, scopeSelector)) {
|
||||
p = strict && !StringWrapper.contains(p, _polyfillHostNoCombinator) ?
|
||||
this._applyStrictSelectorScope(p, scopeSelector) :
|
||||
this._applySelectorScope(p, scopeSelector, hostSelector);
|
||||
}
|
||||
r.push(p);
|
||||
}
|
||||
return r.join(', ');
|
||||
}
|
||||
|
||||
_selectorNeedsScoping(selector: string, scopeSelector: string): boolean {
|
||||
var re = this._makeScopeMatcher(scopeSelector);
|
||||
return !isPresent(RegExpWrapper.firstMatch(re, selector));
|
||||
}
|
||||
|
||||
_makeScopeMatcher(scopeSelector: string): RegExp {
|
||||
var lre = /\[/g;
|
||||
var rre = /\]/g;
|
||||
scopeSelector = StringWrapper.replaceAll(scopeSelector, lre, '\\[');
|
||||
scopeSelector = StringWrapper.replaceAll(scopeSelector, rre, '\\]');
|
||||
return RegExpWrapper.create('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
|
||||
}
|
||||
|
||||
_applySelectorScope(selector: string, scopeSelector: string, hostSelector: string): string {
|
||||
// Difference from webcomponentsjs: scopeSelector could not be an array
|
||||
return this._applySimpleSelectorScope(selector, scopeSelector, hostSelector);
|
||||
}
|
||||
|
||||
// scope via name and [is=name]
|
||||
_applySimpleSelectorScope(selector: string, scopeSelector: string, hostSelector: string): string {
|
||||
if (isPresent(RegExpWrapper.firstMatch(_polyfillHostRe, selector))) {
|
||||
var replaceBy = this.strictStyling ? `[${hostSelector}]` : scopeSelector;
|
||||
selector = StringWrapper.replace(selector, _polyfillHostNoCombinator, replaceBy);
|
||||
return StringWrapper.replaceAll(selector, _polyfillHostRe, replaceBy + ' ');
|
||||
} else {
|
||||
return scopeSelector + ' ' + selector;
|
||||
}
|
||||
}
|
||||
|
||||
// return a selector with [name] suffix on each simple selector
|
||||
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name]
|
||||
_applyStrictSelectorScope(selector: string, scopeSelector: string): string {
|
||||
var isRe = /\[is=([^\]]*)\]/g;
|
||||
scopeSelector = StringWrapper.replaceAllMapped(scopeSelector, isRe, (m) => m[1]);
|
||||
var splits = [' ', '>', '+', '~'], scoped = selector, attrName = '[' + scopeSelector + ']';
|
||||
for (var i = 0; i < splits.length; i++) {
|
||||
var sep = splits[i];
|
||||
var parts = scoped.split(sep);
|
||||
scoped = ListWrapper.map(parts, function(p) {
|
||||
// remove :host since it should be unnecessary
|
||||
var t = StringWrapper.replaceAll(p.trim(), _polyfillHostRe, '');
|
||||
if (t.length > 0 && !ListWrapper.contains(splits, t) &&
|
||||
!StringWrapper.contains(t, attrName)) {
|
||||
var re = /([^:]*)(:*)(.*)/g;
|
||||
var m = RegExpWrapper.firstMatch(re, t);
|
||||
if (isPresent(m)) {
|
||||
p = m[1] + attrName + m[2] + m[3];
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}).join(sep);
|
||||
}
|
||||
return scoped;
|
||||
}
|
||||
|
||||
_insertPolyfillHostInCssText(selector: string): string {
|
||||
selector = StringWrapper.replaceAll(selector, _colonHostContextRe, _polyfillHostContext);
|
||||
selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost);
|
||||
return selector;
|
||||
}
|
||||
|
||||
_propertiesFromRule(rule): string {
|
||||
var cssText = rule.style.cssText;
|
||||
// TODO(sorvell): Safari cssom incorrectly removes quotes from the content
|
||||
// property. (https://bugs.webkit.org/show_bug.cgi?id=118045)
|
||||
// don't replace attr rules
|
||||
var attrRe = /['"]+|attr/g;
|
||||
if (rule.style.content.length > 0 &&
|
||||
!isPresent(RegExpWrapper.firstMatch(attrRe, rule.style.content))) {
|
||||
var contentRe = /content:[^;]*;/g;
|
||||
cssText =
|
||||
StringWrapper.replaceAll(cssText, contentRe, 'content: \'' + rule.style.content + '\';');
|
||||
}
|
||||
// TODO(sorvell): we can workaround this issue here, but we need a list
|
||||
// of troublesome properties to fix https://github.com/Polymer/platform/issues/53
|
||||
//
|
||||
// inherit rules can be omitted from cssText
|
||||
// TODO(sorvell): remove when Blink bug is fixed:
|
||||
// https://code.google.com/p/chromium/issues/detail?id=358273
|
||||
// var style = rule.style;
|
||||
// for (var i = 0; i < style.length; i++) {
|
||||
// var name = style.item(i);
|
||||
// var value = style.getPropertyValue(name);
|
||||
// if (value == 'initial') {
|
||||
// cssText += name + ': initial; ';
|
||||
// }
|
||||
//}
|
||||
return cssText;
|
||||
}
|
||||
}
|
||||
var _cssContentNextSelectorRe =
|
||||
/polyfill-next-selector[^}]*content:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim;
|
||||
var _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim;
|
||||
var _cssContentUnscopedRuleRe =
|
||||
/(polyfill-unscoped-rule)[^}]*(content:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim;
|
||||
var _polyfillHost = '-shadowcsshost';
|
||||
// note: :host-context pre-processed to -shadowcsshostcontext.
|
||||
var _polyfillHostContext = '-shadowcsscontext';
|
||||
var _parenSuffix = ')(?:\\((' +
|
||||
'(?:\\([^)(]*\\)|[^)(]*)+?' +
|
||||
')\\))?([^,{]*)';
|
||||
var _cssColonHostRe = RegExpWrapper.create('(' + _polyfillHost + _parenSuffix, 'im');
|
||||
var _cssColonHostContextRe = RegExpWrapper.create('(' + _polyfillHostContext + _parenSuffix, 'im');
|
||||
var _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
|
||||
var _shadowDOMSelectorsRe = [
|
||||
/>>>/g,
|
||||
/::shadow/g,
|
||||
/::content/g,
|
||||
// Deprecated selectors
|
||||
// TODO(vicb): see https://github.com/angular/clang-format/issues/16
|
||||
// clang-format off
|
||||
/\/deep\//g, // former >>>
|
||||
/\/shadow-deep\//g, // former /deep/
|
||||
/\/shadow\//g, // former ::shadow
|
||||
// clanf-format on
|
||||
];
|
||||
var _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
|
||||
var _polyfillHostRe = RegExpWrapper.create(_polyfillHost, 'im');
|
||||
var _colonHostRe = /:host/gim;
|
||||
var _colonHostContextRe = /:host-context/gim;
|
||||
|
||||
function _cssToRules(cssText: string) {
|
||||
return DOM.cssToRules(cssText);
|
||||
}
|
||||
|
||||
function _withCssRules(cssText: string, callback: Function) {
|
||||
// Difference from webcomponentjs: remove the workaround for an old bug in Chrome
|
||||
if (isBlank(callback)) return;
|
||||
var rules = _cssToRules(cssText);
|
||||
callback(rules);
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import {CompileStep} from '../compiler/compile_step';
|
||||
import {CompileElement} from '../compiler/compile_element';
|
||||
import {CompileControl} from '../compiler/compile_control';
|
||||
import {ViewDefinition, ViewEncapsulation, ViewType} from '../../api';
|
||||
import {NG_CONTENT_ELEMENT_NAME, isElementWithTag} from '../util';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||
import {ShadowCss} from './shadow_css';
|
||||
|
||||
export class StyleEncapsulator implements CompileStep {
|
||||
constructor(private _appId: string, private _view: ViewDefinition,
|
||||
private _componentUIDsCache: Map<string, string>) {}
|
||||
|
||||
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
|
||||
if (isElementWithTag(current.element, NG_CONTENT_ELEMENT_NAME)) {
|
||||
current.inheritedProtoView.bindNgContent();
|
||||
} else {
|
||||
if (this._view.encapsulation === ViewEncapsulation.EMULATED) {
|
||||
this._processEmulatedScopedElement(current, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processStyle(style: string): string {
|
||||
var encapsulation = this._view.encapsulation;
|
||||
if (encapsulation === ViewEncapsulation.EMULATED) {
|
||||
return this._shimCssForComponent(style, this._view.componentId);
|
||||
} else {
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
_processEmulatedScopedElement(current: CompileElement, parent: CompileElement): void {
|
||||
var element = current.element;
|
||||
var hostComponentId = this._view.componentId;
|
||||
var viewType = current.inheritedProtoView.type;
|
||||
// Shim the element as a child of the compiled component
|
||||
if (viewType !== ViewType.HOST && isPresent(hostComponentId)) {
|
||||
var contentAttribute = getContentAttribute(this._getComponentId(hostComponentId));
|
||||
DOM.setAttribute(element, contentAttribute, '');
|
||||
// also shim the host
|
||||
if (isBlank(parent) && viewType == ViewType.COMPONENT) {
|
||||
var hostAttribute = getHostAttribute(this._getComponentId(hostComponentId));
|
||||
current.inheritedProtoView.setHostAttribute(hostAttribute, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_shimCssForComponent(cssText: string, componentId: string): string {
|
||||
var id = this._getComponentId(componentId);
|
||||
var shadowCss = new ShadowCss();
|
||||
return shadowCss.shimCssText(cssText, getContentAttribute(id), getHostAttribute(id));
|
||||
}
|
||||
|
||||
_getComponentId(componentStringId: string): string {
|
||||
var id = this._componentUIDsCache.get(componentStringId);
|
||||
if (isBlank(id)) {
|
||||
id = `${this._appId}-${this._componentUIDsCache.size}`;
|
||||
this._componentUIDsCache.set(componentStringId, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the attribute to be added to the component
|
||||
function getHostAttribute(compId: string): string {
|
||||
return `_nghost-${compId}`;
|
||||
}
|
||||
|
||||
// Returns the attribute to be added on every single element nodes in the component
|
||||
function getContentAttribute(compId: string): string {
|
||||
return `_ngcontent-${compId}`;
|
||||
}
|
138
modules/angular2/src/core/render/dom/compiler/style_inliner.ts
Normal file
138
modules/angular2/src/core/render/dom/compiler/style_inliner.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {XHR} from 'angular2/src/render/xhr';
|
||||
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {UrlResolver} from 'angular2/src/services/url_resolver';
|
||||
import {StyleUrlResolver} from './style_url_resolver';
|
||||
|
||||
import {
|
||||
isBlank,
|
||||
isPresent,
|
||||
RegExp,
|
||||
RegExpWrapper,
|
||||
StringWrapper,
|
||||
normalizeBlank,
|
||||
isPromise
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
Promise,
|
||||
PromiseWrapper,
|
||||
} from 'angular2/src/facade/async';
|
||||
|
||||
/**
|
||||
* Inline @import rules in the given CSS.
|
||||
*
|
||||
* When an @import rules is inlined, it's url are rewritten.
|
||||
*/
|
||||
@Injectable()
|
||||
export class StyleInliner {
|
||||
constructor(public _xhr: XHR, public _styleUrlResolver: StyleUrlResolver,
|
||||
public _urlResolver: UrlResolver) {}
|
||||
|
||||
/**
|
||||
* Inline the @imports rules in the given CSS text.
|
||||
*
|
||||
* The baseUrl is required to rewrite URLs in the inlined content.
|
||||
*
|
||||
* @param {string} cssText
|
||||
* @param {string} baseUrl
|
||||
* @returns {*} a Promise<string> when @import rules are present, a string otherwise
|
||||
*/
|
||||
inlineImports(cssText: string, baseUrl: string): Promise<string>| string {
|
||||
return this._inlineImports(cssText, baseUrl, []);
|
||||
}
|
||||
|
||||
_inlineImports(cssText: string, baseUrl: string, inlinedUrls: List<string>): Promise<string>|
|
||||
string {
|
||||
var partIndex = 0;
|
||||
var parts = StringWrapper.split(cssText, _importRe);
|
||||
|
||||
if (parts.length === 1) {
|
||||
// no @import rule found, return the original css
|
||||
return cssText;
|
||||
}
|
||||
|
||||
var promises = [];
|
||||
|
||||
while (partIndex < parts.length - 1) {
|
||||
// prefix is the content before the @import rule
|
||||
var prefix = parts[partIndex];
|
||||
// rule is the parameter of the @import rule
|
||||
var rule = parts[partIndex + 1];
|
||||
var url = _extractUrl(rule);
|
||||
if (isPresent(url)) {
|
||||
url = this._urlResolver.resolve(baseUrl, url);
|
||||
}
|
||||
var mediaQuery = _extractMediaQuery(rule);
|
||||
var promise;
|
||||
|
||||
if (isBlank(url)) {
|
||||
promise = PromiseWrapper.resolve(`/* Invalid import rule: "@import ${rule};" */`);
|
||||
} else if (ListWrapper.contains(inlinedUrls, url)) {
|
||||
// The current import rule has already been inlined, return the prefix only
|
||||
// Importing again might cause a circular dependency
|
||||
promise = PromiseWrapper.resolve(prefix);
|
||||
} else {
|
||||
inlinedUrls.push(url);
|
||||
promise = PromiseWrapper.then(this._xhr.get(url), (rawCss) => {
|
||||
// resolve nested @import rules
|
||||
var inlinedCss = this._inlineImports(rawCss, url, inlinedUrls);
|
||||
if (isPromise(inlinedCss)) {
|
||||
// wait until nested @import are inlined
|
||||
return (<Promise<string>>inlinedCss)
|
||||
.then((css) => prefix + this._transformImportedCss(css, mediaQuery, url) + '\n');
|
||||
} else {
|
||||
// there are no nested @import, return the css
|
||||
return prefix + this._transformImportedCss(<string>inlinedCss, mediaQuery, url) + '\n';
|
||||
}
|
||||
}, (error) => `/* failed to import ${url} */\n`);
|
||||
}
|
||||
promises.push(promise);
|
||||
partIndex += 2;
|
||||
}
|
||||
|
||||
return PromiseWrapper.all(promises).then(function(cssParts) {
|
||||
var cssText = cssParts.join('');
|
||||
if (partIndex < parts.length) {
|
||||
// append then content located after the last @import rule
|
||||
cssText += parts[partIndex];
|
||||
}
|
||||
return cssText;
|
||||
});
|
||||
}
|
||||
|
||||
_transformImportedCss(css: string, mediaQuery: string, url: string): string {
|
||||
css = this._styleUrlResolver.resolveUrls(css, url);
|
||||
return _wrapInMediaRule(css, mediaQuery);
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts the url from an import rule, supported formats:
|
||||
// - 'url' / "url",
|
||||
// - url(url) / url('url') / url("url")
|
||||
function _extractUrl(importRule: string): string {
|
||||
var match = RegExpWrapper.firstMatch(_urlRe, importRule);
|
||||
if (isBlank(match)) return null;
|
||||
return isPresent(match[1]) ? match[1] : match[2];
|
||||
}
|
||||
|
||||
// Extracts the media query from an import rule.
|
||||
// Returns null when there is no media query.
|
||||
function _extractMediaQuery(importRule: string): string {
|
||||
var match = RegExpWrapper.firstMatch(_mediaQueryRe, importRule);
|
||||
if (isBlank(match)) return null;
|
||||
var mediaQuery = match[1].trim();
|
||||
return (mediaQuery.length > 0) ? mediaQuery : null;
|
||||
}
|
||||
|
||||
// Wraps the css in a media rule when the media query is not null
|
||||
function _wrapInMediaRule(css: string, query: string): string {
|
||||
return (isBlank(query)) ? css : `@media ${query} {\n${css}\n}`;
|
||||
}
|
||||
|
||||
var _importRe = /@import\s+([^;]+);/g;
|
||||
var _urlRe = RegExpWrapper.create(
|
||||
'url\\(\\s*?[\'"]?([^\'")]+)[\'"]?|' + // url(url) or url('url') or url("url")
|
||||
'[\'"]([^\'")]+)[\'"]' // "url" or 'url'
|
||||
);
|
||||
var _mediaQueryRe = /['"][^'"]+['"]\s*\)?\s*(.*)/g;
|
@ -0,0 +1,42 @@
|
||||
// Some of the code comes from WebComponents.JS
|
||||
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
|
||||
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {RegExp, RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
|
||||
import {UrlResolver} from 'angular2/src/services/url_resolver';
|
||||
|
||||
/**
|
||||
* Rewrites URLs by resolving '@import' and 'url()' URLs from the given base URL.
|
||||
*/
|
||||
@Injectable()
|
||||
export class StyleUrlResolver {
|
||||
constructor(public _resolver: UrlResolver) {}
|
||||
|
||||
resolveUrls(cssText: string, baseUrl: string): string {
|
||||
cssText = this._replaceUrls(cssText, _cssUrlRe, baseUrl);
|
||||
cssText = this._replaceUrls(cssText, _cssImportRe, baseUrl);
|
||||
return cssText;
|
||||
}
|
||||
|
||||
_replaceUrls(cssText: string, re: RegExp, baseUrl: string) {
|
||||
return StringWrapper.replaceAllMapped(cssText, re, (m) => {
|
||||
var pre = m[1];
|
||||
var originalUrl = m[2];
|
||||
if (RegExpWrapper.test(_dataUrlRe, originalUrl)) {
|
||||
// Do not attempt to resolve data: URLs
|
||||
return m[0];
|
||||
}
|
||||
var url = StringWrapper.replaceAll(originalUrl, _quoteRe, '');
|
||||
var post = m[3];
|
||||
|
||||
var resolvedUrl = this._resolver.resolve(baseUrl, url);
|
||||
|
||||
return pre + "'" + resolvedUrl + "'" + post;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var _cssUrlRe = /(url\()([^)]*)(\))/g;
|
||||
var _cssImportRe = /(@import[\s]+(?!url\())['"]([^'"]*)['"](.*;)/g;
|
||||
var _quoteRe = /['"]/g;
|
||||
var _dataUrlRe = /^['"]?data:/g;
|
@ -0,0 +1,41 @@
|
||||
import {RegExpWrapper, StringWrapper, isPresent} from 'angular2/src/facade/lang';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import {Parser} from 'angular2/src/change_detection/change_detection';
|
||||
|
||||
import {CompileStep} from './compile_step';
|
||||
import {CompileElement} from './compile_element';
|
||||
import {CompileControl} from './compile_control';
|
||||
|
||||
/**
|
||||
* Parses interpolations in direct text child nodes of the current element.
|
||||
*/
|
||||
export class TextInterpolationParser implements CompileStep {
|
||||
constructor(public _parser: Parser) {}
|
||||
|
||||
processStyle(style: string): string { return style; }
|
||||
|
||||
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
|
||||
if (!current.compileChildren) {
|
||||
return;
|
||||
}
|
||||
var element = current.element;
|
||||
var childNodes = DOM.childNodes(DOM.templateAwareRoot(element));
|
||||
for (var i = 0; i < childNodes.length; i++) {
|
||||
var node = childNodes[i];
|
||||
if (DOM.isTextNode(node)) {
|
||||
var textNode = <Text>node;
|
||||
var text = DOM.nodeValue(textNode);
|
||||
var expr = this._parser.parseInterpolation(text, current.elementDescription);
|
||||
if (isPresent(expr)) {
|
||||
DOM.setText(textNode, ' ');
|
||||
if (current.element === current.inheritedProtoView.rootElement) {
|
||||
current.inheritedProtoView.bindRootText(textNode, expr);
|
||||
} else {
|
||||
current.bindElement().bindText(textNode, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
168
modules/angular2/src/core/render/dom/compiler/view_loader.ts
Normal file
168
modules/angular2/src/core/render/dom/compiler/view_loader.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {
|
||||
isBlank,
|
||||
isPresent,
|
||||
BaseException,
|
||||
stringify,
|
||||
isPromise,
|
||||
StringWrapper
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {Map, MapWrapper, ListWrapper, List} from 'angular2/src/facade/collection';
|
||||
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {ViewDefinition} from '../../api';
|
||||
|
||||
import {XHR} from 'angular2/src/render/xhr';
|
||||
|
||||
import {StyleInliner} from './style_inliner';
|
||||
import {StyleUrlResolver} from './style_url_resolver';
|
||||
import {wtfStartTimeRange, wtfEndTimeRange} from '../../../profile/profile';
|
||||
|
||||
export class TemplateAndStyles {
|
||||
constructor(public template: string, public styles: string[]) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy to load component views.
|
||||
* TODO: Make public API once we are more confident in this approach.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ViewLoader {
|
||||
_cache: Map<string, Promise<string>> = new Map();
|
||||
|
||||
constructor(private _xhr: XHR, private _styleInliner: StyleInliner,
|
||||
private _styleUrlResolver: StyleUrlResolver) {}
|
||||
|
||||
load(viewDef: ViewDefinition): Promise<TemplateAndStyles> {
|
||||
var r = wtfStartTimeRange('ViewLoader#load()', stringify(viewDef.componentId));
|
||||
let tplAndStyles: List<Promise<TemplateAndStyles>| Promise<string>| string> =
|
||||
[this._loadHtml(viewDef.template, viewDef.templateAbsUrl, viewDef.componentId)];
|
||||
if (isPresent(viewDef.styles)) {
|
||||
viewDef.styles.forEach((cssText: string) => {
|
||||
let textOrPromise = this._resolveAndInlineCssText(cssText, viewDef.templateAbsUrl);
|
||||
tplAndStyles.push(textOrPromise);
|
||||
});
|
||||
}
|
||||
|
||||
if (isPresent(viewDef.styleAbsUrls)) {
|
||||
viewDef.styleAbsUrls.forEach(url => {
|
||||
let promise = this._loadText(url).then(
|
||||
cssText => this._resolveAndInlineCssText(cssText, viewDef.templateAbsUrl));
|
||||
tplAndStyles.push(promise);
|
||||
});
|
||||
}
|
||||
|
||||
// Inline the styles from the @View annotation
|
||||
return PromiseWrapper.all(tplAndStyles)
|
||||
.then((res: List<TemplateAndStyles | string>) => {
|
||||
let loadedTplAndStyles = <TemplateAndStyles>res[0];
|
||||
let styles = <string[]>ListWrapper.slice(res, 1);
|
||||
|
||||
var templateAndStyles = new TemplateAndStyles(loadedTplAndStyles.template,
|
||||
loadedTplAndStyles.styles.concat(styles));
|
||||
wtfEndTimeRange(r);
|
||||
return templateAndStyles;
|
||||
});
|
||||
}
|
||||
|
||||
private _loadText(url: string): Promise<string> {
|
||||
var response = this._cache.get(url);
|
||||
|
||||
if (isBlank(response)) {
|
||||
// TODO(vicb): change error when TS gets fixed
|
||||
// https://github.com/angular/angular/issues/2280
|
||||
// throw new BaseException(`Failed to fetch url "${url}"`);
|
||||
response = PromiseWrapper.catchError(
|
||||
this._xhr.get(url),
|
||||
_ => PromiseWrapper.reject(new BaseException(`Failed to fetch url "${url}"`), null));
|
||||
|
||||
this._cache.set(url, response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Load the html and inline any style tags
|
||||
private _loadHtml(template: string, templateAbsUrl: string,
|
||||
componentId: string): Promise<TemplateAndStyles> {
|
||||
let html;
|
||||
|
||||
// Load the HTML
|
||||
if (isPresent(template)) {
|
||||
html = PromiseWrapper.resolve(template);
|
||||
} else if (isPresent(templateAbsUrl)) {
|
||||
html = this._loadText(templateAbsUrl);
|
||||
} else {
|
||||
throw new BaseException(
|
||||
`View should have either the templateUrl or template property set but none was found for the '${componentId}' component`);
|
||||
}
|
||||
|
||||
return html.then(html => {
|
||||
var tplEl = DOM.createTemplate(html);
|
||||
// Replace $baseUrl with the base url for the template
|
||||
if (isPresent(templateAbsUrl) && templateAbsUrl.indexOf("/") >= 0) {
|
||||
let baseUrl = templateAbsUrl.substring(0, templateAbsUrl.lastIndexOf("/"));
|
||||
this._substituteBaseUrl(DOM.content(tplEl), baseUrl);
|
||||
}
|
||||
let styleEls = DOM.querySelectorAll(DOM.content(tplEl), 'STYLE');
|
||||
let unresolvedStyles: string[] = [];
|
||||
for (let i = 0; i < styleEls.length; i++) {
|
||||
var styleEl = styleEls[i];
|
||||
unresolvedStyles.push(DOM.getText(styleEl));
|
||||
DOM.remove(styleEl);
|
||||
}
|
||||
|
||||
let syncStyles: string[] = [];
|
||||
let asyncStyles: Promise<string>[] = [];
|
||||
|
||||
// Inline the style tags from the html
|
||||
for (let i = 0; i < styleEls.length; i++) {
|
||||
let styleEl = styleEls[i];
|
||||
let resolvedStyled = this._resolveAndInlineCssText(DOM.getText(styleEl), templateAbsUrl);
|
||||
if (isPromise(resolvedStyled)) {
|
||||
asyncStyles.push(<Promise<string>>resolvedStyled);
|
||||
} else {
|
||||
syncStyles.push(<string>resolvedStyled);
|
||||
}
|
||||
}
|
||||
|
||||
if (asyncStyles.length === 0) {
|
||||
return PromiseWrapper.resolve(new TemplateAndStyles(DOM.getInnerHTML(tplEl), syncStyles));
|
||||
} else {
|
||||
return PromiseWrapper.all(asyncStyles)
|
||||
.then(loadedStyles => new TemplateAndStyles(DOM.getInnerHTML(tplEl),
|
||||
syncStyles.concat(<string[]>loadedStyles)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all occurrences of $baseUrl in the attributes of an element and its
|
||||
* children with the base URL of the template.
|
||||
*
|
||||
* @param element The element to process
|
||||
* @param baseUrl The base URL of the template.
|
||||
* @private
|
||||
*/
|
||||
private _substituteBaseUrl(element, baseUrl: string): void {
|
||||
if (DOM.isElementNode(element)) {
|
||||
var attrs = DOM.attributeMap(element);
|
||||
MapWrapper.forEach(attrs, (v, k) => {
|
||||
if (isPresent(v) && v.indexOf('$baseUrl') >= 0) {
|
||||
DOM.setAttribute(element, k, StringWrapper.replaceAll(v, /\$baseUrl/g, baseUrl));
|
||||
}
|
||||
});
|
||||
}
|
||||
let children = DOM.childNodes(element);
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (DOM.isElementNode(children[i])) {
|
||||
this._substituteBaseUrl(children[i], baseUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveAndInlineCssText(cssText: string, baseUrl: string): string | Promise<string> {
|
||||
cssText = this._styleUrlResolver.resolveUrls(cssText, baseUrl);
|
||||
return this._styleInliner.inlineImports(cssText, baseUrl);
|
||||
}
|
||||
}
|
124
modules/angular2/src/core/render/dom/compiler/view_splitter.ts
Normal file
124
modules/angular2/src/core/render/dom/compiler/view_splitter.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import {isBlank, isPresent, BaseException, StringWrapper} from 'angular2/src/facade/lang';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {Parser} from 'angular2/src/change_detection/change_detection';
|
||||
|
||||
import {CompileStep} from './compile_step';
|
||||
import {CompileElement} from './compile_element';
|
||||
import {CompileControl} from './compile_control';
|
||||
|
||||
import {dashCaseToCamelCase} from '../util';
|
||||
|
||||
/**
|
||||
* Splits views at `<template>` elements or elements with `template` attribute:
|
||||
* For `<template>` elements:
|
||||
* - moves the content into a new and disconnected `<template>` element
|
||||
* that is marked as view root.
|
||||
*
|
||||
* For elements with a `template` attribute:
|
||||
* - replaces the element with an empty `<template>` element,
|
||||
* parses the content of the `template` attribute and adds the information to that
|
||||
* `<template>` element. Marks the elements as view root.
|
||||
*
|
||||
* Note: In both cases the root of the nested view is disconnected from its parent element.
|
||||
* This is needed for browsers that don't support the `<template>` element
|
||||
* as we want to do locate elements with bindings using `getElementsByClassName` later on,
|
||||
* which should not descend into the nested view.
|
||||
*/
|
||||
export class ViewSplitter implements CompileStep {
|
||||
constructor(public _parser: Parser) {}
|
||||
|
||||
processStyle(style: string): string { return style; }
|
||||
|
||||
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
|
||||
var attrs = current.attrs();
|
||||
var templateBindings = attrs.get('template');
|
||||
var hasTemplateBinding = isPresent(templateBindings);
|
||||
|
||||
// look for template shortcuts such as *ng-if="condition" and treat them as template="if
|
||||
// condition"
|
||||
MapWrapper.forEach(attrs, (attrValue, attrName) => {
|
||||
if (StringWrapper.startsWith(attrName, '*')) {
|
||||
var key = StringWrapper.substring(attrName, 1); // remove the star
|
||||
if (hasTemplateBinding) {
|
||||
// 2nd template binding detected
|
||||
throw new BaseException(`Only one template directive per element is allowed: ` +
|
||||
`${templateBindings} and ${key} cannot be used simultaneously ` +
|
||||
`in ${current.elementDescription}`);
|
||||
} else {
|
||||
templateBindings = (attrValue.length == 0) ? key : key + ' ' + attrValue;
|
||||
hasTemplateBinding = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isPresent(parent)) {
|
||||
if (DOM.isTemplateElement(current.element)) {
|
||||
if (!current.isViewRoot) {
|
||||
var viewRoot = new CompileElement(DOM.createTemplate(''));
|
||||
viewRoot.inheritedProtoView = current.bindElement().bindNestedProtoView(viewRoot.element);
|
||||
// viewRoot doesn't appear in the original template, so we associate
|
||||
// the current element description to get a more meaningful message in case of error
|
||||
viewRoot.elementDescription = current.elementDescription;
|
||||
viewRoot.isViewRoot = true;
|
||||
|
||||
this._moveChildNodes(DOM.content(current.element), DOM.content(viewRoot.element));
|
||||
control.addChild(viewRoot);
|
||||
}
|
||||
}
|
||||
if (hasTemplateBinding) {
|
||||
var anchor = new CompileElement(DOM.createTemplate(''));
|
||||
anchor.inheritedProtoView = current.inheritedProtoView;
|
||||
anchor.inheritedElementBinder = current.inheritedElementBinder;
|
||||
anchor.distanceToInheritedBinder = current.distanceToInheritedBinder;
|
||||
// newParent doesn't appear in the original template, so we associate
|
||||
// the current element description to get a more meaningful message in case of error
|
||||
anchor.elementDescription = current.elementDescription;
|
||||
|
||||
var viewRoot = new CompileElement(DOM.createTemplate(''));
|
||||
viewRoot.inheritedProtoView = anchor.bindElement().bindNestedProtoView(viewRoot.element);
|
||||
// viewRoot doesn't appear in the original template, so we associate
|
||||
// the current element description to get a more meaningful message in case of error
|
||||
viewRoot.elementDescription = current.elementDescription;
|
||||
viewRoot.isViewRoot = true;
|
||||
|
||||
current.inheritedProtoView = viewRoot.inheritedProtoView;
|
||||
current.inheritedElementBinder = null;
|
||||
current.distanceToInheritedBinder = 0;
|
||||
|
||||
this._parseTemplateBindings(templateBindings, anchor);
|
||||
DOM.insertBefore(current.element, anchor.element);
|
||||
control.addParent(anchor);
|
||||
|
||||
DOM.appendChild(DOM.content(viewRoot.element), current.element);
|
||||
control.addParent(viewRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_moveChildNodes(source, target) {
|
||||
var next = DOM.firstChild(source);
|
||||
while (isPresent(next)) {
|
||||
DOM.appendChild(target, next);
|
||||
next = DOM.firstChild(source);
|
||||
}
|
||||
}
|
||||
|
||||
_parseTemplateBindings(templateBindings: string, compileElement: CompileElement) {
|
||||
var bindings =
|
||||
this._parser.parseTemplateBindings(templateBindings, compileElement.elementDescription);
|
||||
for (var i = 0; i < bindings.length; i++) {
|
||||
var binding = bindings[i];
|
||||
if (binding.keyIsVar) {
|
||||
compileElement.bindElement().bindVariable(dashCaseToCamelCase(binding.key), binding.name);
|
||||
compileElement.attrs().set(binding.key, binding.name);
|
||||
} else if (isPresent(binding.expression)) {
|
||||
compileElement.bindElement().bindProperty(dashCaseToCamelCase(binding.key),
|
||||
binding.expression);
|
||||
compileElement.attrs().set(binding.key, binding.expression.source);
|
||||
} else {
|
||||
DOM.setAttribute(compileElement.element, binding.key, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
modules/angular2/src/core/render/dom/convert.ts
Normal file
67
modules/angular2/src/core/render/dom/convert.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
import {isPresent, isArray} from 'angular2/src/facade/lang';
|
||||
import {RenderDirectiveMetadata} from 'angular2/src/render/api';
|
||||
|
||||
/**
|
||||
* Converts a [DirectiveMetadata] to a map representation. This creates a copy,
|
||||
* that is, subsequent changes to `meta` will not be mirrored in the map.
|
||||
*/
|
||||
export function directiveMetadataToMap(meta: RenderDirectiveMetadata): Map<string, any> {
|
||||
return MapWrapper.createFromPairs([
|
||||
['id', meta.id],
|
||||
['selector', meta.selector],
|
||||
['compileChildren', meta.compileChildren],
|
||||
['hostProperties', _cloneIfPresent(meta.hostProperties)],
|
||||
['hostListeners', _cloneIfPresent(meta.hostListeners)],
|
||||
['hostActions', _cloneIfPresent(meta.hostActions)],
|
||||
['hostAttributes', _cloneIfPresent(meta.hostAttributes)],
|
||||
['properties', _cloneIfPresent(meta.properties)],
|
||||
['readAttributes', _cloneIfPresent(meta.readAttributes)],
|
||||
['type', meta.type],
|
||||
['exportAs', meta.exportAs],
|
||||
['callOnDestroy', meta.callOnDestroy],
|
||||
['callOnCheck', meta.callOnCheck],
|
||||
['callOnInit', meta.callOnInit],
|
||||
['callOnChange', meta.callOnChange],
|
||||
['callOnAllChangesDone', meta.callOnAllChangesDone],
|
||||
['events', meta.events],
|
||||
['changeDetection', meta.changeDetection],
|
||||
['version', 1],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a map representation of [DirectiveMetadata] into a
|
||||
* [DirectiveMetadata] object. This creates a copy, that is, subsequent changes
|
||||
* to `map` will not be mirrored in the [DirectiveMetadata] object.
|
||||
*/
|
||||
export function directiveMetadataFromMap(map: Map<string, any>): RenderDirectiveMetadata {
|
||||
return new RenderDirectiveMetadata({
|
||||
id:<string>map.get('id'),
|
||||
selector:<string>map.get('selector'),
|
||||
compileChildren:<boolean>map.get('compileChildren'),
|
||||
hostProperties:<Map<string, string>>_cloneIfPresent(map.get('hostProperties')),
|
||||
hostListeners:<Map<string, string>>_cloneIfPresent(map.get('hostListeners')),
|
||||
hostActions:<Map<string, string>>_cloneIfPresent(map.get('hostActions')),
|
||||
hostAttributes:<Map<string, string>>_cloneIfPresent(map.get('hostAttributes')),
|
||||
properties:<List<string>>_cloneIfPresent(map.get('properties')),
|
||||
readAttributes:<List<string>>_cloneIfPresent(map.get('readAttributes')),
|
||||
type:<number>map.get('type'),
|
||||
exportAs:<string>map.get('exportAs'),
|
||||
callOnDestroy:<boolean>map.get('callOnDestroy'),
|
||||
callOnCheck:<boolean>map.get('callOnCheck'),
|
||||
callOnChange:<boolean>map.get('callOnChange'),
|
||||
callOnInit:<boolean>map.get('callOnInit'),
|
||||
callOnAllChangesDone:<boolean>map.get('callOnAllChangesDone'),
|
||||
events:<List<string>>_cloneIfPresent(map.get('events')),
|
||||
changeDetection:<string>map.get('changeDetection'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the [List] or [Map] `o` if it is present.
|
||||
*/
|
||||
function _cloneIfPresent(o): any {
|
||||
if (!isPresent(o)) return null;
|
||||
return isArray(o) ? ListWrapper.clone(o) : MapWrapper.clone(o);
|
||||
}
|
290
modules/angular2/src/core/render/dom/dom_renderer.ts
Normal file
290
modules/angular2/src/core/render/dom/dom_renderer.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import {Inject, Injectable, OpaqueToken} from 'angular2/di';
|
||||
import {
|
||||
isPresent,
|
||||
isBlank,
|
||||
BaseException,
|
||||
RegExpWrapper,
|
||||
CONST_EXPR
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
|
||||
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import {EventManager} from './events/event_manager';
|
||||
|
||||
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './view/proto_view';
|
||||
import {DomView, DomViewRef, resolveInternalDomView} from './view/view';
|
||||
import {DomFragmentRef, resolveInternalDomFragment} from './view/fragment';
|
||||
import {DomSharedStylesHost} from './view/shared_styles_host';
|
||||
import {
|
||||
NG_BINDING_CLASS_SELECTOR,
|
||||
NG_BINDING_CLASS,
|
||||
cloneAndQueryProtoView,
|
||||
camelCaseToDashCase
|
||||
} from './util';
|
||||
import {WtfScopeFn, wtfLeave, wtfCreateScope} from '../../profile/profile';
|
||||
|
||||
import {
|
||||
Renderer,
|
||||
RenderProtoViewRef,
|
||||
RenderViewRef,
|
||||
RenderElementRef,
|
||||
RenderFragmentRef,
|
||||
RenderViewWithFragments
|
||||
} from '../api';
|
||||
|
||||
import {TemplateCloner} from './template_cloner';
|
||||
|
||||
import {DOCUMENT} from './dom_tokens';
|
||||
|
||||
const REFLECT_PREFIX: string = 'ng-reflect-';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class DomRenderer extends Renderer {
|
||||
_document;
|
||||
|
||||
constructor(private _eventManager: EventManager,
|
||||
private _domSharedStylesHost: DomSharedStylesHost,
|
||||
private _templateCloner: TemplateCloner, @Inject(DOCUMENT) document) {
|
||||
super();
|
||||
this._document = document;
|
||||
}
|
||||
|
||||
_scope_createRootHostView: WtfScopeFn = wtfCreateScope('DomRenderer#createRootHostView()');
|
||||
createRootHostView(hostProtoViewRef: RenderProtoViewRef, fragmentCount: number,
|
||||
hostElementSelector: string): RenderViewWithFragments {
|
||||
var s = this._scope_createRootHostView();
|
||||
var hostProtoView = resolveInternalDomProtoView(hostProtoViewRef);
|
||||
var element = DOM.querySelector(this._document, hostElementSelector);
|
||||
if (isBlank(element)) {
|
||||
wtfLeave(s);
|
||||
throw new BaseException(`The selector "${hostElementSelector}" did not match any elements`);
|
||||
}
|
||||
return wtfLeave(s, this._createView(hostProtoView, element));
|
||||
}
|
||||
|
||||
_scope_createView = wtfCreateScope('DomRenderer#createView()');
|
||||
createView(protoViewRef: RenderProtoViewRef, fragmentCount: number): RenderViewWithFragments {
|
||||
var s = this._scope_createView();
|
||||
var protoView = resolveInternalDomProtoView(protoViewRef);
|
||||
return wtfLeave(s, this._createView(protoView, null));
|
||||
}
|
||||
|
||||
destroyView(viewRef: RenderViewRef) {
|
||||
var view = resolveInternalDomView(viewRef);
|
||||
var elementBinders = view.proto.elementBinders;
|
||||
for (var i = 0; i < elementBinders.length; i++) {
|
||||
var binder = elementBinders[i];
|
||||
if (binder.hasNativeShadowRoot) {
|
||||
this._domSharedStylesHost.removeHost(DOM.getShadowRoot(view.boundElements[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getNativeElementSync(location: RenderElementRef): any {
|
||||
if (isBlank(location.renderBoundElementIndex)) {
|
||||
return null;
|
||||
}
|
||||
return resolveInternalDomView(location.renderView)
|
||||
.boundElements[location.renderBoundElementIndex];
|
||||
}
|
||||
|
||||
getRootNodes(fragment: RenderFragmentRef): List<Node> {
|
||||
return resolveInternalDomFragment(fragment);
|
||||
}
|
||||
|
||||
attachFragmentAfterFragment(previousFragmentRef: RenderFragmentRef,
|
||||
fragmentRef: RenderFragmentRef) {
|
||||
var previousFragmentNodes = resolveInternalDomFragment(previousFragmentRef);
|
||||
if (previousFragmentNodes.length > 0) {
|
||||
var sibling = previousFragmentNodes[previousFragmentNodes.length - 1];
|
||||
moveNodesAfterSibling(sibling, resolveInternalDomFragment(fragmentRef));
|
||||
}
|
||||
}
|
||||
|
||||
attachFragmentAfterElement(elementRef: RenderElementRef, fragmentRef: RenderFragmentRef) {
|
||||
if (isBlank(elementRef.renderBoundElementIndex)) {
|
||||
return;
|
||||
}
|
||||
var parentView = resolveInternalDomView(elementRef.renderView);
|
||||
var element = parentView.boundElements[elementRef.renderBoundElementIndex];
|
||||
moveNodesAfterSibling(element, resolveInternalDomFragment(fragmentRef));
|
||||
}
|
||||
|
||||
_scope_detachFragment = wtfCreateScope('DomRenderer#detachFragment()');
|
||||
detachFragment(fragmentRef: RenderFragmentRef) {
|
||||
var s = this._scope_detachFragment();
|
||||
var fragmentNodes = resolveInternalDomFragment(fragmentRef);
|
||||
for (var i = 0; i < fragmentNodes.length; i++) {
|
||||
DOM.remove(fragmentNodes[i]);
|
||||
}
|
||||
wtfLeave(s);
|
||||
}
|
||||
|
||||
hydrateView(viewRef: RenderViewRef) {
|
||||
var view = resolveInternalDomView(viewRef);
|
||||
if (view.hydrated) throw new BaseException('The view is already hydrated.');
|
||||
view.hydrated = true;
|
||||
|
||||
// add global events
|
||||
view.eventHandlerRemovers = [];
|
||||
var binders = view.proto.elementBinders;
|
||||
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
|
||||
var binder = binders[binderIdx];
|
||||
if (isPresent(binder.globalEvents)) {
|
||||
for (var i = 0; i < binder.globalEvents.length; i++) {
|
||||
var globalEvent = binder.globalEvents[i];
|
||||
var remover = this._createGlobalEventListener(view, binderIdx, globalEvent.name,
|
||||
globalEvent.target, globalEvent.fullName);
|
||||
view.eventHandlerRemovers.push(remover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dehydrateView(viewRef: RenderViewRef) {
|
||||
var view = resolveInternalDomView(viewRef);
|
||||
|
||||
// remove global events
|
||||
for (var i = 0; i < view.eventHandlerRemovers.length; i++) {
|
||||
view.eventHandlerRemovers[i]();
|
||||
}
|
||||
|
||||
view.eventHandlerRemovers = null;
|
||||
view.hydrated = false;
|
||||
}
|
||||
|
||||
setElementProperty(location: RenderElementRef, propertyName: string, propertyValue: any): void {
|
||||
if (isBlank(location.renderBoundElementIndex)) {
|
||||
return;
|
||||
}
|
||||
var view = resolveInternalDomView(location.renderView);
|
||||
view.setElementProperty(location.renderBoundElementIndex, propertyName, propertyValue);
|
||||
}
|
||||
|
||||
setElementAttribute(location: RenderElementRef, attributeName: string, attributeValue: string):
|
||||
void {
|
||||
if (isBlank(location.renderBoundElementIndex)) {
|
||||
return;
|
||||
}
|
||||
var view = resolveInternalDomView(location.renderView);
|
||||
view.setElementAttribute(location.renderBoundElementIndex, attributeName, attributeValue);
|
||||
}
|
||||
|
||||
setElementClass(location: RenderElementRef, className: string, isAdd: boolean): void {
|
||||
if (isBlank(location.renderBoundElementIndex)) {
|
||||
return;
|
||||
}
|
||||
var view = resolveInternalDomView(location.renderView);
|
||||
view.setElementClass(location.renderBoundElementIndex, className, isAdd);
|
||||
}
|
||||
|
||||
setElementStyle(location: RenderElementRef, styleName: string, styleValue: string): void {
|
||||
if (isBlank(location.renderBoundElementIndex)) {
|
||||
return;
|
||||
}
|
||||
var view = resolveInternalDomView(location.renderView);
|
||||
view.setElementStyle(location.renderBoundElementIndex, styleName, styleValue);
|
||||
}
|
||||
|
||||
invokeElementMethod(location: RenderElementRef, methodName: string, args: List<any>): void {
|
||||
if (isBlank(location.renderBoundElementIndex)) {
|
||||
return;
|
||||
}
|
||||
var view = resolveInternalDomView(location.renderView);
|
||||
view.invokeElementMethod(location.renderBoundElementIndex, methodName, args);
|
||||
}
|
||||
|
||||
setText(viewRef: RenderViewRef, textNodeIndex: number, text: string): void {
|
||||
if (isBlank(textNodeIndex)) {
|
||||
return;
|
||||
}
|
||||
var view = resolveInternalDomView(viewRef);
|
||||
DOM.setText(view.boundTextNodes[textNodeIndex], text);
|
||||
}
|
||||
|
||||
_scope_setEventDispatcher = wtfCreateScope('DomRenderer#setEventDispatcher()');
|
||||
setEventDispatcher(viewRef: RenderViewRef, dispatcher: any /*api.EventDispatcher*/): void {
|
||||
var s = this._scope_setEventDispatcher();
|
||||
var view = resolveInternalDomView(viewRef);
|
||||
view.eventDispatcher = dispatcher;
|
||||
wtfLeave(s);
|
||||
}
|
||||
|
||||
_createView(protoView: DomProtoView, inplaceElement: HTMLElement): RenderViewWithFragments {
|
||||
var clonedProtoView = cloneAndQueryProtoView(this._templateCloner, protoView, true);
|
||||
|
||||
var boundElements = clonedProtoView.boundElements;
|
||||
|
||||
// adopt inplaceElement
|
||||
if (isPresent(inplaceElement)) {
|
||||
if (protoView.fragmentsRootNodeCount[0] !== 1) {
|
||||
throw new BaseException('Root proto views can only contain one element!');
|
||||
}
|
||||
DOM.clearNodes(inplaceElement);
|
||||
var tempRoot = clonedProtoView.fragments[0][0];
|
||||
moveChildNodes(tempRoot, inplaceElement);
|
||||
if (boundElements.length > 0 && boundElements[0] === tempRoot) {
|
||||
boundElements[0] = inplaceElement;
|
||||
}
|
||||
clonedProtoView.fragments[0][0] = inplaceElement;
|
||||
}
|
||||
|
||||
var view = new DomView(protoView, clonedProtoView.boundTextNodes, boundElements);
|
||||
|
||||
var binders = protoView.elementBinders;
|
||||
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
|
||||
var binder = binders[binderIdx];
|
||||
var element = boundElements[binderIdx];
|
||||
|
||||
// native shadow DOM
|
||||
if (binder.hasNativeShadowRoot) {
|
||||
var shadowRootWrapper = DOM.firstChild(element);
|
||||
var shadowRoot = DOM.createShadowRoot(element);
|
||||
this._domSharedStylesHost.addHost(shadowRoot);
|
||||
moveChildNodes(shadowRootWrapper, shadowRoot);
|
||||
DOM.remove(shadowRootWrapper);
|
||||
}
|
||||
|
||||
// events
|
||||
if (isPresent(binder.eventLocals) && isPresent(binder.localEvents)) {
|
||||
for (var i = 0; i < binder.localEvents.length; i++) {
|
||||
this._createEventListener(view, element, binderIdx, binder.localEvents[i].name,
|
||||
binder.eventLocals);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new RenderViewWithFragments(
|
||||
new DomViewRef(view), clonedProtoView.fragments.map(nodes => new DomFragmentRef(nodes)));
|
||||
}
|
||||
|
||||
_createEventListener(view, element, elementIndex, eventName, eventLocals) {
|
||||
this._eventManager.addEventListener(
|
||||
element, eventName, (event) => { view.dispatchEvent(elementIndex, eventName, event); });
|
||||
}
|
||||
|
||||
_createGlobalEventListener(view, elementIndex, eventName, eventTarget, fullName): Function {
|
||||
return this._eventManager.addGlobalEventListener(
|
||||
eventTarget, eventName, (event) => { view.dispatchEvent(elementIndex, fullName, event); });
|
||||
}
|
||||
}
|
||||
|
||||
function moveNodesAfterSibling(sibling, nodes) {
|
||||
if (nodes.length > 0 && isPresent(DOM.parentElement(sibling))) {
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
DOM.insertBefore(sibling, nodes[i]);
|
||||
}
|
||||
DOM.insertBefore(nodes[nodes.length - 1], sibling);
|
||||
}
|
||||
}
|
||||
|
||||
function moveChildNodes(source: Node, target: Node) {
|
||||
var currChild = DOM.firstChild(source);
|
||||
while (isPresent(currChild)) {
|
||||
var nextChild = DOM.nextSibling(currChild);
|
||||
DOM.appendChild(target, currChild);
|
||||
currChild = nextChild;
|
||||
}
|
||||
}
|
30
modules/angular2/src/core/render/dom/dom_tokens.ts
Normal file
30
modules/angular2/src/core/render/dom/dom_tokens.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {OpaqueToken, Binding} from 'angular2/di';
|
||||
import {CONST_EXPR, StringWrapper, Math} from 'angular2/src/facade/lang';
|
||||
|
||||
export const DOCUMENT: OpaqueToken = CONST_EXPR(new OpaqueToken('DocumentToken'));
|
||||
|
||||
/**
|
||||
* A unique id (string) for an angular application.
|
||||
*/
|
||||
export const APP_ID: OpaqueToken = CONST_EXPR(new OpaqueToken('AppId'));
|
||||
|
||||
function _appIdRandomBindingFactory() {
|
||||
return `${randomChar()}${randomChar()}${randomChar()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindings that will generate a random APP_ID_TOKEN.
|
||||
*/
|
||||
export const APP_ID_RANDOM_BINDING: Binding =
|
||||
CONST_EXPR(new Binding(APP_ID, {toFactory: _appIdRandomBindingFactory, deps: []}));
|
||||
|
||||
/**
|
||||
* Defines when a compiled template should be stored as a string
|
||||
* rather than keeping its Nodes to preserve memory.
|
||||
*/
|
||||
export const MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE: OpaqueToken =
|
||||
CONST_EXPR(new OpaqueToken('MaxInMemoryElementsPerTemplate'));
|
||||
|
||||
function randomChar(): string {
|
||||
return StringWrapper.fromCharCode(97 + Math.floor(Math.random() * 25));
|
||||
}
|
109
modules/angular2/src/core/render/dom/events/event_manager.ts
Normal file
109
modules/angular2/src/core/render/dom/events/event_manager.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import {isBlank, BaseException, isPresent, StringWrapper} from 'angular2/src/facade/lang';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
import {NgZone} from 'angular2/src/core/zone/ng_zone';
|
||||
|
||||
var BUBBLE_SYMBOL = '^';
|
||||
|
||||
export class EventManager {
|
||||
constructor(public _plugins: List<EventManagerPlugin>, public _zone: NgZone) {
|
||||
for (var i = 0; i < _plugins.length; i++) {
|
||||
_plugins[i].manager = this;
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener(element: HTMLElement, eventName: string, handler: Function) {
|
||||
var withoutBubbleSymbol = this._removeBubbleSymbol(eventName);
|
||||
var plugin = this._findPluginFor(withoutBubbleSymbol);
|
||||
plugin.addEventListener(element, withoutBubbleSymbol, handler,
|
||||
withoutBubbleSymbol != eventName);
|
||||
}
|
||||
|
||||
addGlobalEventListener(target: string, eventName: string, handler: Function): Function {
|
||||
var withoutBubbleSymbol = this._removeBubbleSymbol(eventName);
|
||||
var plugin = this._findPluginFor(withoutBubbleSymbol);
|
||||
return plugin.addGlobalEventListener(target, withoutBubbleSymbol, handler,
|
||||
withoutBubbleSymbol != eventName);
|
||||
}
|
||||
|
||||
getZone(): NgZone { return this._zone; }
|
||||
|
||||
_findPluginFor(eventName: string): EventManagerPlugin {
|
||||
var plugins = this._plugins;
|
||||
for (var i = 0; i < plugins.length; i++) {
|
||||
var plugin = plugins[i];
|
||||
if (plugin.supports(eventName)) {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
throw new BaseException(`No event manager plugin found for event ${eventName}`);
|
||||
}
|
||||
|
||||
_removeBubbleSymbol(eventName: string): string {
|
||||
return eventName[0] == BUBBLE_SYMBOL ? StringWrapper.substring(eventName, 1) : eventName;
|
||||
}
|
||||
}
|
||||
|
||||
export class EventManagerPlugin {
|
||||
manager: EventManager;
|
||||
|
||||
// We are assuming here that all plugins support bubbled and non-bubbled events.
|
||||
// That is equivalent to having supporting $event.target
|
||||
// The bubbling flag (currently ^) is stripped before calling the supports and
|
||||
// addEventListener methods.
|
||||
supports(eventName: string): boolean { return false; }
|
||||
|
||||
addEventListener(element: HTMLElement, eventName: string, handler: Function,
|
||||
shouldSupportBubble: boolean) {
|
||||
throw "not implemented";
|
||||
}
|
||||
|
||||
addGlobalEventListener(element: string, eventName: string, handler: Function,
|
||||
shouldSupportBubble: boolean): Function {
|
||||
throw "not implemented";
|
||||
}
|
||||
}
|
||||
|
||||
export class DomEventsPlugin extends EventManagerPlugin {
|
||||
manager: EventManager;
|
||||
|
||||
// This plugin should come last in the list of plugins, because it accepts all
|
||||
// events.
|
||||
supports(eventName: string): boolean { return true; }
|
||||
|
||||
addEventListener(element: HTMLElement, eventName: string, handler: Function,
|
||||
shouldSupportBubble: boolean) {
|
||||
var outsideHandler =
|
||||
this._getOutsideHandler(shouldSupportBubble, element, handler, this.manager._zone);
|
||||
this.manager._zone.runOutsideAngular(() => { DOM.on(element, eventName, outsideHandler); });
|
||||
}
|
||||
|
||||
addGlobalEventListener(target: string, eventName: string, handler: Function,
|
||||
shouldSupportBubble: boolean): Function {
|
||||
var element = DOM.getGlobalEventTarget(target);
|
||||
var outsideHandler =
|
||||
this._getOutsideHandler(shouldSupportBubble, element, handler, this.manager._zone);
|
||||
return this.manager._zone.runOutsideAngular(
|
||||
() => { return DOM.onAndCancel(element, eventName, outsideHandler); });
|
||||
}
|
||||
|
||||
_getOutsideHandler(shouldSupportBubble: boolean, element: HTMLElement, handler: Function,
|
||||
zone: NgZone) {
|
||||
return shouldSupportBubble ? DomEventsPlugin.bubbleCallback(element, handler, zone) :
|
||||
DomEventsPlugin.sameElementCallback(element, handler, zone);
|
||||
}
|
||||
|
||||
static sameElementCallback(element: HTMLElement, handler: Function, zone: NgZone):
|
||||
(event: Event) => void {
|
||||
return (event) => {
|
||||
if (event.target === element) {
|
||||
zone.run(() => handler(event));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static bubbleCallback(element: HTMLElement, handler: Function, zone: NgZone):
|
||||
(event: Event) => void {
|
||||
return (event) => zone.run(() => handler(event));
|
||||
}
|
||||
}
|
50
modules/angular2/src/core/render/dom/events/hammer_common.ts
Normal file
50
modules/angular2/src/core/render/dom/events/hammer_common.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import {EventManagerPlugin} from './event_manager';
|
||||
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
var _eventNames = {
|
||||
// pan
|
||||
'pan': true,
|
||||
'panstart': true,
|
||||
'panmove': true,
|
||||
'panend': true,
|
||||
'pancancel': true,
|
||||
'panleft': true,
|
||||
'panright': true,
|
||||
'panup': true,
|
||||
'pandown': true,
|
||||
// pinch
|
||||
'pinch': true,
|
||||
'pinchstart': true,
|
||||
'pinchmove': true,
|
||||
'pinchend': true,
|
||||
'pinchcancel': true,
|
||||
'pinchin': true,
|
||||
'pinchout': true,
|
||||
// press
|
||||
'press': true,
|
||||
'pressup': true,
|
||||
// rotate
|
||||
'rotate': true,
|
||||
'rotatestart': true,
|
||||
'rotatemove': true,
|
||||
'rotateend': true,
|
||||
'rotatecancel': true,
|
||||
// swipe
|
||||
'swipe': true,
|
||||
'swipeleft': true,
|
||||
'swiperight': true,
|
||||
'swipeup': true,
|
||||
'swipedown': true,
|
||||
// tap
|
||||
'tap': true,
|
||||
};
|
||||
|
||||
|
||||
export class HammerGesturesPluginCommon extends EventManagerPlugin {
|
||||
constructor() { super(); }
|
||||
|
||||
supports(eventName: string): boolean {
|
||||
eventName = eventName.toLowerCase();
|
||||
return StringMapWrapper.contains(_eventNames, eventName);
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
library angular.events;
|
||||
|
||||
import 'dart:html';
|
||||
import './hammer_common.dart';
|
||||
import 'package:angular2/src/facade/lang.dart' show BaseException;
|
||||
|
||||
import 'dart:js' as js;
|
||||
|
||||
class HammerGesturesPlugin extends HammerGesturesPluginCommon {
|
||||
bool supports(String eventName) {
|
||||
if (!super.supports(eventName)) return false;
|
||||
|
||||
if (!js.context.hasProperty('Hammer')) {
|
||||
throw new BaseException(
|
||||
'Hammer.js is not loaded, can not bind ${eventName} event');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
addEventListener(Element element, String eventName, Function handler,
|
||||
bool shouldSupportBubble) {
|
||||
if (shouldSupportBubble) throw new BaseException(
|
||||
'Hammer.js plugin does not support bubbling gestures.');
|
||||
var zone = this.manager.getZone();
|
||||
eventName = eventName.toLowerCase();
|
||||
|
||||
zone.runOutsideAngular(() {
|
||||
// Creating the manager bind events, must be done outside of angular
|
||||
var mc = new js.JsObject(js.context['Hammer'], [element]);
|
||||
|
||||
var jsObj = mc.callMethod('get', ['pinch']);
|
||||
jsObj.callMethod('set', [
|
||||
new js.JsObject.jsify({'enable': true})
|
||||
]);
|
||||
jsObj = mc.callMethod('get', ['rotate']);
|
||||
jsObj.callMethod('set', [
|
||||
new js.JsObject.jsify({'enable': true})
|
||||
]);
|
||||
|
||||
mc.callMethod('on', [
|
||||
eventName,
|
||||
(eventObj) {
|
||||
zone.run(() {
|
||||
var dartEvent = new HammerEvent._fromJsEvent(eventObj);
|
||||
handler(dartEvent);
|
||||
});
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class HammerEvent {
|
||||
num angle;
|
||||
num centerX;
|
||||
num centerY;
|
||||
int deltaTime;
|
||||
int deltaX;
|
||||
int deltaY;
|
||||
int direction;
|
||||
int distance;
|
||||
num rotation;
|
||||
num scale;
|
||||
Node target;
|
||||
int timeStamp;
|
||||
String type;
|
||||
num velocity;
|
||||
num velocityX;
|
||||
num velocityY;
|
||||
js.JsObject jsEvent;
|
||||
|
||||
HammerEvent._fromJsEvent(js.JsObject event) {
|
||||
angle = event['angle'];
|
||||
var center = event['center'];
|
||||
centerX = center['x'];
|
||||
centerY = center['y'];
|
||||
deltaTime = event['deltaTime'];
|
||||
deltaX = event['deltaX'];
|
||||
deltaY = event['deltaY'];
|
||||
direction = event['direction'];
|
||||
distance = event['distance'];
|
||||
rotation = event['rotation'];
|
||||
scale = event['scale'];
|
||||
target = event['target'];
|
||||
timeStamp = event['timeStamp'];
|
||||
type = event['type'];
|
||||
velocity = event['velocity'];
|
||||
velocityX = event['velocityX'];
|
||||
velocityY = event['velocityY'];
|
||||
jsEvent = event;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/// <reference path="../../../../typings/hammerjs/hammerjs.d.ts"/>
|
||||
|
||||
import {HammerGesturesPluginCommon} from './hammer_common';
|
||||
import {isPresent, BaseException} from 'angular2/src/facade/lang';
|
||||
|
||||
export class HammerGesturesPlugin extends HammerGesturesPluginCommon {
|
||||
constructor() { super(); }
|
||||
|
||||
supports(eventName: string): boolean {
|
||||
if (!super.supports(eventName)) return false;
|
||||
|
||||
if (!isPresent(window['Hammer'])) {
|
||||
throw new BaseException(`Hammer.js is not loaded, can not bind ${eventName} event`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
addEventListener(element: HTMLElement, eventName: string, handler: Function,
|
||||
shouldSupportBubble: boolean) {
|
||||
if (shouldSupportBubble)
|
||||
throw new BaseException('Hammer.js plugin does not support bubbling gestures.');
|
||||
var zone = this.manager.getZone();
|
||||
eventName = eventName.toLowerCase();
|
||||
|
||||
zone.runOutsideAngular(function() {
|
||||
// Creating the manager bind events, must be done outside of angular
|
||||
var mc = new Hammer(element);
|
||||
mc.get('pinch').set({enable: true});
|
||||
mc.get('rotate').set({enable: true});
|
||||
|
||||
mc.on(eventName, function(eventObj) { zone.run(function() { handler(eventObj); }); });
|
||||
});
|
||||
}
|
||||
}
|
113
modules/angular2/src/core/render/dom/events/key_events.ts
Normal file
113
modules/angular2/src/core/render/dom/events/key_events.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {
|
||||
isPresent,
|
||||
isBlank,
|
||||
StringWrapper,
|
||||
RegExpWrapper,
|
||||
BaseException,
|
||||
NumberWrapper
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {EventManagerPlugin} from './event_manager';
|
||||
import {NgZone} from 'angular2/src/core/zone/ng_zone';
|
||||
|
||||
var modifierKeys = ['alt', 'control', 'meta', 'shift'];
|
||||
var modifierKeyGetters: StringMap<string, Function> = {
|
||||
'alt': (event) => event.altKey,
|
||||
'control': (event) => event.ctrlKey,
|
||||
'meta': (event) => event.metaKey,
|
||||
'shift': (event) => event.shiftKey
|
||||
};
|
||||
|
||||
export class KeyEventsPlugin extends EventManagerPlugin {
|
||||
constructor() { super(); }
|
||||
|
||||
supports(eventName: string): boolean {
|
||||
return isPresent(KeyEventsPlugin.parseEventName(eventName));
|
||||
}
|
||||
|
||||
addEventListener(element: HTMLElement, eventName: string, handler: (Event: any) => any,
|
||||
shouldSupportBubble: boolean) {
|
||||
var parsedEvent = KeyEventsPlugin.parseEventName(eventName);
|
||||
|
||||
var outsideHandler = KeyEventsPlugin.eventCallback(element, shouldSupportBubble,
|
||||
StringMapWrapper.get(parsedEvent, 'fullKey'),
|
||||
handler, this.manager.getZone());
|
||||
|
||||
this.manager.getZone().runOutsideAngular(() => {
|
||||
DOM.on(element, StringMapWrapper.get(parsedEvent, 'domEventName'), outsideHandler);
|
||||
});
|
||||
}
|
||||
|
||||
static parseEventName(eventName: string): StringMap<string, string> {
|
||||
var parts = eventName.toLowerCase().split('.');
|
||||
|
||||
var domEventName = ListWrapper.removeAt(parts, 0);
|
||||
if ((parts.length === 0) ||
|
||||
!(StringWrapper.equals(domEventName, 'keydown') ||
|
||||
StringWrapper.equals(domEventName, 'keyup'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var key = KeyEventsPlugin._normalizeKey(ListWrapper.removeLast(parts));
|
||||
|
||||
var fullKey = '';
|
||||
ListWrapper.forEach(modifierKeys, (modifierName) => {
|
||||
if (ListWrapper.contains(parts, modifierName)) {
|
||||
ListWrapper.remove(parts, modifierName);
|
||||
fullKey += modifierName + '.';
|
||||
}
|
||||
});
|
||||
fullKey += key;
|
||||
|
||||
if (parts.length != 0 || key.length === 0) {
|
||||
// returning null instead of throwing to let another plugin process the event
|
||||
return null;
|
||||
}
|
||||
var result = StringMapWrapper.create();
|
||||
StringMapWrapper.set(result, 'domEventName', domEventName);
|
||||
StringMapWrapper.set(result, 'fullKey', fullKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
static getEventFullKey(event: Event): string {
|
||||
var fullKey = '';
|
||||
var key = DOM.getEventKey(event);
|
||||
key = key.toLowerCase();
|
||||
if (StringWrapper.equals(key, ' ')) {
|
||||
key = 'space'; // for readability
|
||||
} else if (StringWrapper.equals(key, '.')) {
|
||||
key = 'dot'; // because '.' is used as a separator in event names
|
||||
}
|
||||
ListWrapper.forEach(modifierKeys, (modifierName) => {
|
||||
if (modifierName != key) {
|
||||
var modifierGetter = StringMapWrapper.get(modifierKeyGetters, modifierName);
|
||||
if (modifierGetter(event)) {
|
||||
fullKey += modifierName + '.';
|
||||
}
|
||||
}
|
||||
});
|
||||
fullKey += key;
|
||||
return fullKey;
|
||||
}
|
||||
|
||||
static eventCallback(element: HTMLElement, shouldSupportBubble: boolean, fullKey: any,
|
||||
handler: (Event) => any, zone: NgZone): (event: Event) => void {
|
||||
return (event) => {
|
||||
var correctElement = shouldSupportBubble || event.target === element;
|
||||
if (correctElement && StringWrapper.equals(KeyEventsPlugin.getEventFullKey(event), fullKey)) {
|
||||
zone.run(() => handler(event));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static _normalizeKey(keyName: string): string {
|
||||
// TODO: switch to a StringMap if the mapping grows too much
|
||||
switch (keyName) {
|
||||
case 'esc':
|
||||
return 'escape';
|
||||
default:
|
||||
return keyName;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import {ElementSchemaRegistry} from './element_schema_registry';
|
||||
|
||||
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
||||
hasProperty(elm: any, propName: string): boolean {
|
||||
var tagName = DOM.tagName(elm);
|
||||
if (tagName.indexOf('-') !== -1) {
|
||||
// can't tell now as we don't know which properties a custom element will get
|
||||
// once it is instantiated
|
||||
return true;
|
||||
} else {
|
||||
return DOM.hasProperty(elm, propName);
|
||||
}
|
||||
}
|
||||
|
||||
getMappedPropName(propName: string): string {
|
||||
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, propName);
|
||||
return isPresent(mappedPropName) ? mappedPropName : propName;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export class ElementSchemaRegistry {
|
||||
hasProperty(elm: any, propName: string): boolean { return true; }
|
||||
getMappedPropName(propName: string): string { return propName; }
|
||||
}
|
47
modules/angular2/src/core/render/dom/template_cloner.ts
Normal file
47
modules/angular2/src/core/render/dom/template_cloner.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import {isString} from 'angular2/src/facade/lang';
|
||||
import {Injectable, Inject} from 'angular2/di';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE} from './dom_tokens';
|
||||
|
||||
@Injectable()
|
||||
export class TemplateCloner {
|
||||
maxInMemoryElementsPerTemplate: number;
|
||||
|
||||
constructor(@Inject(MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE) maxInMemoryElementsPerTemplate) {
|
||||
this.maxInMemoryElementsPerTemplate = maxInMemoryElementsPerTemplate;
|
||||
}
|
||||
|
||||
prepareForClone(templateRoot: Element): Element | string {
|
||||
var elementCount = DOM.querySelectorAll(DOM.content(templateRoot), '*').length;
|
||||
if (this.maxInMemoryElementsPerTemplate >= 0 &&
|
||||
elementCount >= this.maxInMemoryElementsPerTemplate) {
|
||||
return DOM.getInnerHTML(templateRoot);
|
||||
} else {
|
||||
return templateRoot;
|
||||
}
|
||||
}
|
||||
|
||||
cloneContent(preparedTemplateRoot: Element | string, importNode: boolean): Node {
|
||||
var templateContent;
|
||||
if (isString(preparedTemplateRoot)) {
|
||||
templateContent = DOM.content(DOM.createTemplate(preparedTemplateRoot));
|
||||
if (importNode) {
|
||||
// Attention: We can't use document.adoptNode here
|
||||
// as this does NOT wake up custom elements in Chrome 43
|
||||
// TODO: Use div.innerHTML instead of template.innerHTML when we
|
||||
// have code to support the various special cases and
|
||||
// don't use importNode additionally (e.g. for <tr>, svg elements, ...)
|
||||
// see https://github.com/angular/angular/issues/3364
|
||||
templateContent = DOM.importIntoDoc(templateContent);
|
||||
}
|
||||
} else {
|
||||
templateContent = DOM.content(<Element>preparedTemplateRoot);
|
||||
if (importNode) {
|
||||
templateContent = DOM.importIntoDoc(templateContent);
|
||||
} else {
|
||||
templateContent = DOM.clone(templateContent);
|
||||
}
|
||||
}
|
||||
return templateContent;
|
||||
}
|
||||
}
|
169
modules/angular2/src/core/render/dom/util.ts
Normal file
169
modules/angular2/src/core/render/dom/util.ts
Normal file
@ -0,0 +1,169 @@
|
||||
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';
|
||||
import {TemplateCloner} from './template_cloner';
|
||||
|
||||
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(); });
|
||||
}
|
||||
|
||||
export class EventConfig {
|
||||
constructor(public fieldName: string, public eventName: string, public isLongForm: boolean) {}
|
||||
|
||||
static parse(eventConfig: string): EventConfig {
|
||||
var fieldName = eventConfig, eventName = eventConfig, isLongForm = false;
|
||||
var separatorIdx = eventConfig.indexOf(EVENT_TARGET_SEPARATOR);
|
||||
if (separatorIdx > -1) {
|
||||
// long format: 'fieldName: eventName'
|
||||
fieldName = StringWrapper.substring(eventConfig, 0, separatorIdx).trim();
|
||||
eventName = StringWrapper.substring(eventConfig, separatorIdx + 1).trim();
|
||||
isLongForm = true;
|
||||
}
|
||||
return new EventConfig(fieldName, eventName, isLongForm);
|
||||
}
|
||||
|
||||
getFullName(): string {
|
||||
return this.isLongForm ? `${this.fieldName}${EVENT_TARGET_SEPARATOR}${this.eventName}` :
|
||||
this.eventName;
|
||||
}
|
||||
}
|
||||
|
||||
// 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(templateCloner: TemplateCloner, pv: DomProtoView,
|
||||
importIntoDocument: boolean): ClonedProtoView {
|
||||
var templateContent = templateCloner.cloneContent(pv.cloneableTemplate, 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;
|
||||
// Note: the 2nd, 3rd, ... fragments are separated by each other via a '|'
|
||||
if (fragmentIndex >= 1) {
|
||||
childNode = DOM.nextSibling(childNode);
|
||||
}
|
||||
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;
|
||||
});
|
||||
}
|
37
modules/angular2/src/core/render/dom/view/element_binder.ts
Normal file
37
modules/angular2/src/core/render/dom/view/element_binder.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import {AST} from 'angular2/src/change_detection/change_detection';
|
||||
import {List, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
|
||||
export class DomElementBinder {
|
||||
textNodeIndices: List<number>;
|
||||
hasNestedProtoView: boolean;
|
||||
eventLocals: AST;
|
||||
localEvents: List<Event>;
|
||||
globalEvents: List<Event>;
|
||||
hasNativeShadowRoot: boolean;
|
||||
|
||||
constructor({textNodeIndices, hasNestedProtoView, eventLocals, localEvents, globalEvents,
|
||||
hasNativeShadowRoot}: {
|
||||
textNodeIndices?: List<number>,
|
||||
hasNestedProtoView?: boolean,
|
||||
eventLocals?: AST,
|
||||
localEvents?: List<Event>,
|
||||
globalEvents?: List<Event>,
|
||||
hasNativeShadowRoot?: boolean
|
||||
} = {}) {
|
||||
this.textNodeIndices = textNodeIndices;
|
||||
this.hasNestedProtoView = hasNestedProtoView;
|
||||
this.eventLocals = eventLocals;
|
||||
this.localEvents = localEvents;
|
||||
this.globalEvents = globalEvents;
|
||||
this.hasNativeShadowRoot = isPresent(hasNativeShadowRoot) ? hasNativeShadowRoot : false;
|
||||
}
|
||||
}
|
||||
|
||||
export class Event {
|
||||
constructor(public name: string, public target: string, public fullName: string) {}
|
||||
}
|
||||
|
||||
export class HostAction {
|
||||
constructor(public actionName: string, public actionExpression: string, public expression: AST) {}
|
||||
}
|
9
modules/angular2/src/core/render/dom/view/fragment.ts
Normal file
9
modules/angular2/src/core/render/dom/view/fragment.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {RenderFragmentRef} from '../../api';
|
||||
|
||||
export function resolveInternalDomFragment(fragmentRef: RenderFragmentRef): Node[] {
|
||||
return (<DomFragmentRef>fragmentRef)._nodes;
|
||||
}
|
||||
|
||||
export class DomFragmentRef extends RenderFragmentRef {
|
||||
constructor(public _nodes: Node[]) { super(); }
|
||||
}
|
41
modules/angular2/src/core/render/dom/view/proto_view.ts
Normal file
41
modules/angular2/src/core/render/dom/view/proto_view.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {List, ListWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {DomElementBinder} from './element_binder';
|
||||
import {RenderProtoViewRef, ViewType, ViewEncapsulation} from '../../api';
|
||||
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import {TemplateCloner} from '../template_cloner';
|
||||
|
||||
export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef): DomProtoView {
|
||||
return (<DomProtoViewRef>protoViewRef)._protoView;
|
||||
}
|
||||
|
||||
export class DomProtoViewRef extends RenderProtoViewRef {
|
||||
constructor(public _protoView: DomProtoView) { super(); }
|
||||
}
|
||||
|
||||
export class DomProtoView {
|
||||
static create(templateCloner: TemplateCloner, type: ViewType, rootElement: Element,
|
||||
viewEncapsulation: ViewEncapsulation, fragmentsRootNodeCount: number[],
|
||||
rootTextNodeIndices: number[], elementBinders: List<DomElementBinder>,
|
||||
hostAttributes: Map<string, string>): DomProtoView {
|
||||
var boundTextNodeCount = rootTextNodeIndices.length;
|
||||
for (var i = 0; i < elementBinders.length; i++) {
|
||||
boundTextNodeCount += elementBinders[i].textNodeIndices.length;
|
||||
}
|
||||
var isSingleElementFragment = fragmentsRootNodeCount.length === 1 &&
|
||||
fragmentsRootNodeCount[0] === 1 &&
|
||||
DOM.isElementNode(DOM.firstChild(DOM.content(rootElement)));
|
||||
return new DomProtoView(type, templateCloner.prepareForClone(rootElement), viewEncapsulation,
|
||||
elementBinders, hostAttributes, rootTextNodeIndices, boundTextNodeCount,
|
||||
fragmentsRootNodeCount, isSingleElementFragment);
|
||||
}
|
||||
// Note: fragments are separated by a comment node that is not counted in fragmentsRootNodeCount!
|
||||
constructor(public type: ViewType, public cloneableTemplate: Element | string,
|
||||
public encapsulation: ViewEncapsulation,
|
||||
public elementBinders: List<DomElementBinder>,
|
||||
public hostAttributes: Map<string, string>, public rootTextNodeIndices: number[],
|
||||
public boundTextNodeCount: number, public fragmentsRootNodeCount: number[],
|
||||
public isSingleElementFragment: boolean) {}
|
||||
}
|
402
modules/angular2/src/core/render/dom/view/proto_view_builder.ts
Normal file
402
modules/angular2/src/core/render/dom/view/proto_view_builder.ts
Normal file
@ -0,0 +1,402 @@
|
||||
import {isPresent, isBlank, BaseException, StringWrapper} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
ListWrapper,
|
||||
MapWrapper,
|
||||
Set,
|
||||
SetWrapper,
|
||||
List,
|
||||
StringMapWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import {
|
||||
ASTWithSource,
|
||||
AST,
|
||||
AstTransformer,
|
||||
PropertyRead,
|
||||
LiteralArray,
|
||||
ImplicitReceiver
|
||||
} from 'angular2/src/change_detection/change_detection';
|
||||
|
||||
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
|
||||
import {DomElementBinder, Event, HostAction} from './element_binder';
|
||||
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
|
||||
import {TemplateCloner} from '../template_cloner';
|
||||
|
||||
import {
|
||||
ViewType,
|
||||
ViewEncapsulation,
|
||||
ProtoViewDto,
|
||||
DirectiveBinder,
|
||||
RenderElementBinder,
|
||||
EventBinding,
|
||||
ElementPropertyBinding,
|
||||
PropertyBindingType
|
||||
} from '../../api';
|
||||
|
||||
import {
|
||||
NG_BINDING_CLASS,
|
||||
EVENT_TARGET_SEPARATOR,
|
||||
queryBoundTextNodeIndices,
|
||||
camelCaseToDashCase
|
||||
} from '../util';
|
||||
|
||||
export class ProtoViewBuilder {
|
||||
variableBindings: Map<string, string> = new Map();
|
||||
elements: List<ElementBinderBuilder> = [];
|
||||
rootTextBindings: Map<Node, ASTWithSource> = new Map();
|
||||
ngContentCount: number = 0;
|
||||
hostAttributes: Map<string, string> = new Map();
|
||||
|
||||
constructor(public rootElement, public type: ViewType,
|
||||
public viewEncapsulation: ViewEncapsulation) {}
|
||||
|
||||
bindElement(element: HTMLElement, description: string = null): ElementBinderBuilder {
|
||||
var builder = new ElementBinderBuilder(this.elements.length, element, description);
|
||||
this.elements.push(builder);
|
||||
DOM.addClass(element, NG_BINDING_CLASS);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
bindVariable(name: string, value: string) {
|
||||
// Store the variable map from value to variable, reflecting how it will be used later by
|
||||
// DomView. When a local is set to the view, a lookup for the variable name will take place
|
||||
// keyed
|
||||
// by the "value", or exported identifier. For example, ng-for sets a view local of "index".
|
||||
// When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
|
||||
// it.
|
||||
this.variableBindings.set(value, name);
|
||||
}
|
||||
|
||||
// Note: We don't store the node index until the compilation is complete,
|
||||
// as the compiler might change the order of elements.
|
||||
bindRootText(textNode: Text, expression: ASTWithSource) {
|
||||
this.rootTextBindings.set(textNode, expression);
|
||||
}
|
||||
|
||||
bindNgContent() { this.ngContentCount++; }
|
||||
|
||||
setHostAttribute(name: string, value: string) { this.hostAttributes.set(name, value); }
|
||||
|
||||
build(schemaRegistry: ElementSchemaRegistry, templateCloner: TemplateCloner): ProtoViewDto {
|
||||
var domElementBinders = [];
|
||||
|
||||
var apiElementBinders = [];
|
||||
var textNodeExpressions = [];
|
||||
var rootTextNodeIndices = [];
|
||||
var transitiveNgContentCount = this.ngContentCount;
|
||||
queryBoundTextNodeIndices(DOM.content(this.rootElement), this.rootTextBindings,
|
||||
(node, nodeIndex, expression) => {
|
||||
textNodeExpressions.push(expression);
|
||||
rootTextNodeIndices.push(nodeIndex);
|
||||
});
|
||||
|
||||
ListWrapper.forEach(this.elements, (ebb: ElementBinderBuilder) => {
|
||||
var directiveTemplatePropertyNames = new Set();
|
||||
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (dbb: DirectiveBuilder) => {
|
||||
ebb.eventBuilder.merge(dbb.eventBuilder);
|
||||
ListWrapper.forEach(dbb.templatePropertyNames,
|
||||
(name) => directiveTemplatePropertyNames.add(name));
|
||||
return new DirectiveBinder({
|
||||
directiveIndex: dbb.directiveIndex,
|
||||
propertyBindings: dbb.propertyBindings,
|
||||
eventBindings: dbb.eventBindings,
|
||||
hostPropertyBindings: buildElementPropertyBindings(schemaRegistry, ebb.element, true,
|
||||
dbb.hostPropertyBindings, null)
|
||||
});
|
||||
});
|
||||
var nestedProtoView = isPresent(ebb.nestedProtoView) ?
|
||||
ebb.nestedProtoView.build(schemaRegistry, templateCloner) :
|
||||
null;
|
||||
if (isPresent(nestedProtoView)) {
|
||||
transitiveNgContentCount += nestedProtoView.transitiveNgContentCount;
|
||||
}
|
||||
var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1;
|
||||
var textNodeIndices = [];
|
||||
queryBoundTextNodeIndices(ebb.element, ebb.textBindings, (node, nodeIndex, expression) => {
|
||||
textNodeExpressions.push(expression);
|
||||
textNodeIndices.push(nodeIndex);
|
||||
});
|
||||
apiElementBinders.push(new RenderElementBinder({
|
||||
index: ebb.index,
|
||||
parentIndex: parentIndex,
|
||||
distanceToParent: ebb.distanceToParent,
|
||||
directives: apiDirectiveBinders,
|
||||
nestedProtoView: nestedProtoView,
|
||||
propertyBindings:
|
||||
buildElementPropertyBindings(schemaRegistry, ebb.element, isPresent(ebb.componentId),
|
||||
ebb.propertyBindings, directiveTemplatePropertyNames),
|
||||
variableBindings: ebb.variableBindings,
|
||||
eventBindings: ebb.eventBindings,
|
||||
readAttributes: ebb.readAttributes
|
||||
}));
|
||||
domElementBinders.push(new DomElementBinder({
|
||||
textNodeIndices: textNodeIndices,
|
||||
hasNestedProtoView: isPresent(nestedProtoView) || isPresent(ebb.componentId),
|
||||
hasNativeShadowRoot: false,
|
||||
eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()),
|
||||
localEvents: ebb.eventBuilder.buildLocalEvents(),
|
||||
globalEvents: ebb.eventBuilder.buildGlobalEvents()
|
||||
}));
|
||||
});
|
||||
var rootNodeCount = DOM.childNodes(DOM.content(this.rootElement)).length;
|
||||
return new ProtoViewDto({
|
||||
render: new DomProtoViewRef(DomProtoView.create(
|
||||
templateCloner, this.type, this.rootElement, this.viewEncapsulation, [rootNodeCount],
|
||||
rootTextNodeIndices, domElementBinders, this.hostAttributes)),
|
||||
type: this.type,
|
||||
elementBinders: apiElementBinders,
|
||||
variableBindings: this.variableBindings,
|
||||
textBindings: textNodeExpressions,
|
||||
transitiveNgContentCount: transitiveNgContentCount
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ElementBinderBuilder {
|
||||
parent: ElementBinderBuilder = null;
|
||||
distanceToParent: number = 0;
|
||||
directives: List<DirectiveBuilder> = [];
|
||||
nestedProtoView: ProtoViewBuilder = null;
|
||||
propertyBindings: Map<string, ASTWithSource> = new Map();
|
||||
variableBindings: Map<string, string> = new Map();
|
||||
eventBindings: List<EventBinding> = [];
|
||||
eventBuilder: EventBuilder = new EventBuilder();
|
||||
textBindings: Map<Node, ASTWithSource> = new Map();
|
||||
readAttributes: Map<string, string> = new Map();
|
||||
componentId: string = null;
|
||||
|
||||
constructor(public index: number, public element, description: string) {}
|
||||
|
||||
setParent(parent: ElementBinderBuilder, distanceToParent: number): ElementBinderBuilder {
|
||||
this.parent = parent;
|
||||
if (isPresent(parent)) {
|
||||
this.distanceToParent = distanceToParent;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
readAttribute(attrName: string) {
|
||||
if (isBlank(this.readAttributes.get(attrName))) {
|
||||
this.readAttributes.set(attrName, DOM.getAttribute(this.element, attrName));
|
||||
}
|
||||
}
|
||||
|
||||
bindDirective(directiveIndex: number): DirectiveBuilder {
|
||||
var directive = new DirectiveBuilder(directiveIndex);
|
||||
this.directives.push(directive);
|
||||
return directive;
|
||||
}
|
||||
|
||||
bindNestedProtoView(rootElement: HTMLElement): ProtoViewBuilder {
|
||||
if (isPresent(this.nestedProtoView)) {
|
||||
throw new BaseException('Only one nested view per element is allowed');
|
||||
}
|
||||
this.nestedProtoView =
|
||||
new ProtoViewBuilder(rootElement, ViewType.EMBEDDED, ViewEncapsulation.NONE);
|
||||
return this.nestedProtoView;
|
||||
}
|
||||
|
||||
bindProperty(name: string, expression: ASTWithSource) {
|
||||
this.propertyBindings.set(name, expression);
|
||||
}
|
||||
|
||||
bindVariable(name: string, value: string) {
|
||||
// When current is a view root, the variable bindings are set to the *nested* proto view.
|
||||
// The root view conceptually signifies a new "block scope" (the nested view), to which
|
||||
// the variables are bound.
|
||||
if (isPresent(this.nestedProtoView)) {
|
||||
this.nestedProtoView.bindVariable(name, value);
|
||||
} else {
|
||||
// Store the variable map from value to variable, reflecting how it will be used later by
|
||||
// DomView. When a local is set to the view, a lookup for the variable name will take place
|
||||
// keyed
|
||||
// by the "value", or exported identifier. For example, ng-for sets a view local of "index".
|
||||
// When this occurs, a lookup keyed by "index" must occur to find if there is a var
|
||||
// referencing
|
||||
// it.
|
||||
this.variableBindings.set(value, name);
|
||||
}
|
||||
}
|
||||
|
||||
bindEvent(name: string, expression: ASTWithSource, target: string = null) {
|
||||
this.eventBindings.push(this.eventBuilder.add(name, expression, target));
|
||||
}
|
||||
|
||||
// Note: We don't store the node index until the compilation is complete,
|
||||
// as the compiler might change the order of elements.
|
||||
bindText(textNode: Text, expression: ASTWithSource) {
|
||||
this.textBindings.set(textNode, expression);
|
||||
}
|
||||
|
||||
setComponentId(componentId: string) { this.componentId = componentId; }
|
||||
}
|
||||
|
||||
export class DirectiveBuilder {
|
||||
// mapping from directive property name to AST for that directive
|
||||
propertyBindings: Map<string, ASTWithSource> = new Map();
|
||||
// property names used in the template
|
||||
templatePropertyNames: List<string> = [];
|
||||
hostPropertyBindings: Map<string, ASTWithSource> = new Map();
|
||||
eventBindings: List<EventBinding> = [];
|
||||
eventBuilder: EventBuilder = new EventBuilder();
|
||||
|
||||
constructor(public directiveIndex: number) {}
|
||||
|
||||
bindProperty(name: string, expression: ASTWithSource, elProp: string) {
|
||||
this.propertyBindings.set(name, expression);
|
||||
if (isPresent(elProp)) {
|
||||
// we are filling in a set of property names that are bound to a property
|
||||
// of at least one directive. This allows us to report "dangling" bindings.
|
||||
this.templatePropertyNames.push(elProp);
|
||||
}
|
||||
}
|
||||
|
||||
bindHostProperty(name: string, expression: ASTWithSource) {
|
||||
this.hostPropertyBindings.set(name, expression);
|
||||
}
|
||||
|
||||
bindEvent(name: string, expression: ASTWithSource, target: string = null) {
|
||||
this.eventBindings.push(this.eventBuilder.add(name, expression, target));
|
||||
}
|
||||
}
|
||||
|
||||
class EventBuilder extends AstTransformer {
|
||||
locals: List<AST> = [];
|
||||
localEvents: List<Event> = [];
|
||||
globalEvents: List<Event> = [];
|
||||
_implicitReceiver: AST = new ImplicitReceiver();
|
||||
|
||||
constructor() { super(); }
|
||||
|
||||
add(name: string, source: ASTWithSource, target: string): EventBinding {
|
||||
// TODO(tbosch): reenable this when we are parsing element properties
|
||||
// out of action expressions
|
||||
// var adjustedAst = astWithSource.ast.visit(this);
|
||||
var adjustedAst = source.ast;
|
||||
var fullName = isPresent(target) ? target + EVENT_TARGET_SEPARATOR + name : name;
|
||||
var result =
|
||||
new EventBinding(fullName, new ASTWithSource(adjustedAst, source.source, source.location));
|
||||
var event = new Event(name, target, fullName);
|
||||
if (isBlank(target)) {
|
||||
this.localEvents.push(event);
|
||||
} else {
|
||||
this.globalEvents.push(event);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
visitPropertyRead(ast: PropertyRead): PropertyRead {
|
||||
var isEventAccess = false;
|
||||
var current: AST = ast;
|
||||
while (!isEventAccess && (current instanceof PropertyRead)) {
|
||||
var am = <PropertyRead>current;
|
||||
if (am.name == '$event') {
|
||||
isEventAccess = true;
|
||||
}
|
||||
current = am.receiver;
|
||||
}
|
||||
|
||||
if (isEventAccess) {
|
||||
this.locals.push(ast);
|
||||
var index = this.locals.length - 1;
|
||||
return new PropertyRead(this._implicitReceiver, `${index}`, (arr) => arr[index]);
|
||||
} else {
|
||||
return ast;
|
||||
}
|
||||
}
|
||||
|
||||
buildEventLocals(): List<AST> { return this.locals; }
|
||||
|
||||
buildLocalEvents(): List<Event> { return this.localEvents; }
|
||||
|
||||
buildGlobalEvents(): List<Event> { return this.globalEvents; }
|
||||
|
||||
merge(eventBuilder: EventBuilder) {
|
||||
this._merge(this.localEvents, eventBuilder.localEvents);
|
||||
this._merge(this.globalEvents, eventBuilder.globalEvents);
|
||||
ListWrapper.concat(this.locals, eventBuilder.locals);
|
||||
}
|
||||
|
||||
_merge(host: List<Event>, tobeAdded: List<Event>) {
|
||||
var names = [];
|
||||
for (var i = 0; i < host.length; i++) {
|
||||
names.push(host[i].fullName);
|
||||
}
|
||||
for (var j = 0; j < tobeAdded.length; j++) {
|
||||
if (!ListWrapper.contains(names, tobeAdded[j].fullName)) {
|
||||
host.push(tobeAdded[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var PROPERTY_PARTS_SEPARATOR = new RegExp('\\.');
|
||||
const ATTRIBUTE_PREFIX = 'attr';
|
||||
const CLASS_PREFIX = 'class';
|
||||
const STYLE_PREFIX = 'style';
|
||||
|
||||
function buildElementPropertyBindings(
|
||||
schemaRegistry: ElementSchemaRegistry, protoElement: /*element*/ any, isNgComponent: boolean,
|
||||
bindingsInTemplate: Map<string, ASTWithSource>, directiveTemplatePropertyNames: Set<string>):
|
||||
List<ElementPropertyBinding> {
|
||||
var propertyBindings = [];
|
||||
|
||||
MapWrapper.forEach(bindingsInTemplate, (ast, propertyNameInTemplate) => {
|
||||
var propertyBinding = createElementPropertyBinding(schemaRegistry, ast, propertyNameInTemplate);
|
||||
|
||||
if (isPresent(directiveTemplatePropertyNames) &&
|
||||
SetWrapper.has(directiveTemplatePropertyNames, propertyNameInTemplate)) {
|
||||
// We do nothing because directives shadow native elements properties.
|
||||
|
||||
} else if (isValidElementPropertyBinding(schemaRegistry, protoElement, isNgComponent,
|
||||
propertyBinding)) {
|
||||
propertyBindings.push(propertyBinding);
|
||||
|
||||
} else {
|
||||
var exMsg =
|
||||
`Can't bind to '${propertyNameInTemplate}' since it isn't a known property of the '<${DOM.tagName(protoElement).toLowerCase()}>' element`;
|
||||
|
||||
// directiveTemplatePropertyNames is null for host property bindings
|
||||
if (isPresent(directiveTemplatePropertyNames)) {
|
||||
exMsg += ' and there are no matching directives with a corresponding property';
|
||||
}
|
||||
throw new BaseException(exMsg);
|
||||
}
|
||||
});
|
||||
return propertyBindings;
|
||||
}
|
||||
|
||||
function isValidElementPropertyBinding(schemaRegistry: ElementSchemaRegistry,
|
||||
protoElement: /*element*/ any, isNgComponent: boolean,
|
||||
binding: ElementPropertyBinding): boolean {
|
||||
if (binding.type === PropertyBindingType.PROPERTY) {
|
||||
if (!isNgComponent) {
|
||||
return schemaRegistry.hasProperty(protoElement, binding.property);
|
||||
} else {
|
||||
// TODO(pk): change this logic as soon as we can properly detect custom elements
|
||||
return DOM.hasProperty(protoElement, binding.property);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function createElementPropertyBinding(schemaRegistry: ElementSchemaRegistry, ast: ASTWithSource,
|
||||
propertyNameInTemplate: string): ElementPropertyBinding {
|
||||
var parts = StringWrapper.split(propertyNameInTemplate, PROPERTY_PARTS_SEPARATOR);
|
||||
if (parts.length === 1) {
|
||||
var propName = schemaRegistry.getMappedPropName(parts[0]);
|
||||
return new ElementPropertyBinding(PropertyBindingType.PROPERTY, ast, propName);
|
||||
} else if (parts[0] == ATTRIBUTE_PREFIX) {
|
||||
return new ElementPropertyBinding(PropertyBindingType.ATTRIBUTE, ast, parts[1]);
|
||||
} else if (parts[0] == CLASS_PREFIX) {
|
||||
return new ElementPropertyBinding(PropertyBindingType.CLASS, ast,
|
||||
camelCaseToDashCase(parts[1]));
|
||||
} else if (parts[0] == STYLE_PREFIX) {
|
||||
var unit = parts.length > 2 ? parts[2] : null;
|
||||
return new ElementPropertyBinding(PropertyBindingType.STYLE, ast, parts[1], unit);
|
||||
} else {
|
||||
throw new BaseException(`Invalid property name ${propertyNameInTemplate}`);
|
||||
}
|
||||
}
|
450
modules/angular2/src/core/render/dom/view/proto_view_merger.ts
Normal file
450
modules/angular2/src/core/render/dom/view/proto_view_merger.ts
Normal file
@ -0,0 +1,450 @@
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {isPresent, isBlank, BaseException, isArray} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, SetWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
|
||||
import {DomElementBinder} from './element_binder';
|
||||
import {
|
||||
RenderProtoViewMergeMapping,
|
||||
RenderProtoViewRef,
|
||||
ViewType,
|
||||
ViewEncapsulation
|
||||
} from '../../api';
|
||||
import {
|
||||
NG_BINDING_CLASS,
|
||||
NG_CONTENT_ELEMENT_NAME,
|
||||
ClonedProtoView,
|
||||
cloneAndQueryProtoView,
|
||||
queryBoundElements,
|
||||
queryBoundTextNodeIndices,
|
||||
NG_SHADOW_ROOT_ELEMENT_NAME,
|
||||
isElementWithTag,
|
||||
prependAll
|
||||
} from '../util';
|
||||
|
||||
import {TemplateCloner} from '../template_cloner';
|
||||
|
||||
export function mergeProtoViewsRecursively(templateCloner: TemplateCloner,
|
||||
protoViewRefs: List<RenderProtoViewRef | List<any>>):
|
||||
RenderProtoViewMergeMapping {
|
||||
// clone
|
||||
var clonedProtoViews = [];
|
||||
var hostViewAndBinderIndices: number[][] = [];
|
||||
cloneProtoViews(templateCloner, protoViewRefs, clonedProtoViews, hostViewAndBinderIndices);
|
||||
var mainProtoView: ClonedProtoView = clonedProtoViews[0];
|
||||
|
||||
// modify the DOM
|
||||
mergeEmbeddedPvsIntoComponentOrRootPv(clonedProtoViews, hostViewAndBinderIndices);
|
||||
var fragments = [];
|
||||
var elementsWithNativeShadowRoot: Set<Element> = new Set();
|
||||
mergeComponents(clonedProtoViews, hostViewAndBinderIndices, fragments,
|
||||
elementsWithNativeShadowRoot);
|
||||
// Note: Need to remark parent elements of bound text nodes
|
||||
// so that we can find them later via queryBoundElements!
|
||||
markBoundTextNodeParentsAsBoundElements(clonedProtoViews);
|
||||
|
||||
// create a new root element with the changed fragments and elements
|
||||
var fragmentsRootNodeCount = fragments.map(fragment => fragment.length);
|
||||
var rootElement = createRootElementFromFragments(fragments);
|
||||
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, elementsWithNativeShadowRoot,
|
||||
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(templateCloner, mainProtoView.original.type, rootElement,
|
||||
mainProtoView.original.encapsulation, fragmentsRootNodeCount,
|
||||
rootTextNodeIndices, mergedElementBinders, new Map());
|
||||
return new RenderProtoViewMergeMapping(new DomProtoViewRef(mergedProtoView),
|
||||
fragmentsRootNodeCount.length, mappedElementIndices,
|
||||
mergedBoundElements.length, mappedTextIndices,
|
||||
hostElementIndicesByViewIndex, nestedViewCounts);
|
||||
}
|
||||
|
||||
function cloneProtoViews(
|
||||
templateCloner: TemplateCloner, protoViewRefs: List<RenderProtoViewRef | List<any>>,
|
||||
targetClonedProtoViews: ClonedProtoView[], targetHostViewAndBinderIndices: number[][]) {
|
||||
var hostProtoView = resolveInternalDomProtoView(protoViewRefs[0]);
|
||||
var hostPvIdx = targetClonedProtoViews.length;
|
||||
targetClonedProtoViews.push(cloneAndQueryProtoView(templateCloner, 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(templateCloner, <any[]>nestedEntry, targetClonedProtoViews,
|
||||
targetHostViewAndBinderIndices);
|
||||
} else {
|
||||
targetClonedProtoViews.push(cloneAndQueryProtoView(
|
||||
templateCloner, resolveInternalDomProtoView(nestedEntry), false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function markBoundTextNodeParentsAsBoundElements(mergableProtoViews: ClonedProtoView[]) {
|
||||
mergableProtoViews.forEach((mergableProtoView) => {
|
||||
mergableProtoView.boundTextNodes.forEach((textNode) => {
|
||||
var parentNode = textNode.parentNode;
|
||||
if (isPresent(parentNode) && DOM.isElementNode(parentNode)) {
|
||||
DOM.addClass(parentNode, NG_BINDING_CLASS);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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[][],
|
||||
targetElementsWithNativeShadowRoot: Set<Element>) {
|
||||
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,
|
||||
targetElementsWithNativeShadowRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeComponent(hostProtoView: ClonedProtoView, binderIdx: number,
|
||||
nestedProtoView: ClonedProtoView, targetFragments: Node[][],
|
||||
targetElementsWithNativeShadowRoot: Set<Element>) {
|
||||
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);
|
||||
var useNativeShadowRoot = nestedProtoView.original.encapsulation === ViewEncapsulation.NATIVE;
|
||||
if (useNativeShadowRoot) {
|
||||
targetElementsWithNativeShadowRoot.add(hostElement);
|
||||
}
|
||||
MapWrapper.forEach(nestedProtoView.original.hostAttributes, (attrValue, attrName) => {
|
||||
DOM.setAttribute(hostElement, attrName, attrValue);
|
||||
});
|
||||
appendComponentNodesToHost(hostProtoView, binderIdx, fragments[0], useNativeShadowRoot);
|
||||
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[], useNativeShadowRoot: boolean) {
|
||||
var hostElement = hostProtoView.boundElements[binderIdx];
|
||||
if (useNativeShadowRoot) {
|
||||
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 = [];
|
||||
DOM.insertBefore(contentElement, DOM.createComment('['));
|
||||
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.insertBefore(contentElement, DOM.createComment(']'));
|
||||
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);
|
||||
for (var i = 0; i < fragments.length; i++) {
|
||||
var fragment = fragments[i];
|
||||
if (i >= 1) {
|
||||
// Note: We need to seprate fragments by a comment so that sibling
|
||||
// text nodes don't get merged when we serialize the DomProtoView into a string
|
||||
// and parse it back again.
|
||||
DOM.appendChild(rootNode, DOM.createComment('|'));
|
||||
}
|
||||
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[],
|
||||
elementsWithNativeShadowRoot: Set<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(
|
||||
updateElementBinders(elementBinderByElement.get(element), textNodeIndices,
|
||||
SetWrapper.has(elementsWithNativeShadowRoot, element)));
|
||||
}
|
||||
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 updateElementBinders(elementBinder: DomElementBinder, textNodeIndices: number[],
|
||||
hasNativeShadowRoot: boolean): DomElementBinder {
|
||||
var result;
|
||||
if (isBlank(elementBinder)) {
|
||||
result = new DomElementBinder({
|
||||
textNodeIndices: textNodeIndices,
|
||||
hasNestedProtoView: false,
|
||||
eventLocals: null,
|
||||
localEvents: [],
|
||||
globalEvents: [],
|
||||
hasNativeShadowRoot: false
|
||||
});
|
||||
} else {
|
||||
result = new DomElementBinder({
|
||||
textNodeIndices: textNodeIndices,
|
||||
hasNestedProtoView: false,
|
||||
eventLocals: elementBinder.eventLocals,
|
||||
localEvents: elementBinder.localEvents,
|
||||
globalEvents: elementBinder.globalEvents,
|
||||
hasNativeShadowRoot: 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;
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {Inject, Injectable} from 'angular2/di';
|
||||
import {SetWrapper} from 'angular2/src/facade/collection';
|
||||
import {DOCUMENT} from '../dom_tokens';
|
||||
|
||||
@Injectable()
|
||||
export class SharedStylesHost {
|
||||
protected _styles: string[] = [];
|
||||
protected _stylesSet: Set<string> = new Set();
|
||||
|
||||
constructor() {}
|
||||
|
||||
addStyles(styles: string[]) {
|
||||
var additions = [];
|
||||
styles.forEach(style => {
|
||||
if (!SetWrapper.has(this._stylesSet, style)) {
|
||||
this._stylesSet.add(style);
|
||||
this._styles.push(style);
|
||||
additions.push(style);
|
||||
}
|
||||
});
|
||||
this.onStylesAdded(additions);
|
||||
}
|
||||
|
||||
protected onStylesAdded(additions: string[]) {}
|
||||
|
||||
getAllStyles(): string[] { return this._styles; }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DomSharedStylesHost extends SharedStylesHost {
|
||||
private _hostNodes: Set<Node> = new Set();
|
||||
constructor(@Inject(DOCUMENT) doc: any) {
|
||||
super();
|
||||
this._hostNodes.add(doc.head);
|
||||
}
|
||||
_addStylesToHost(styles: string[], host: Node) {
|
||||
for (var i = 0; i < styles.length; i++) {
|
||||
var style = styles[i];
|
||||
DOM.appendChild(host, DOM.createStyleElement(style));
|
||||
}
|
||||
}
|
||||
addHost(hostNode: Node) {
|
||||
this._addStylesToHost(this._styles, hostNode);
|
||||
this._hostNodes.add(hostNode);
|
||||
}
|
||||
removeHost(hostNode: Node) { SetWrapper.delete(this._hostNodes, hostNode); }
|
||||
|
||||
onStylesAdded(additions: string[]) {
|
||||
this._hostNodes.forEach((hostNode) => { this._addStylesToHost(additions, hostNode); });
|
||||
}
|
||||
}
|
87
modules/angular2/src/core/render/dom/view/view.ts
Normal file
87
modules/angular2/src/core/render/dom/view/view.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
|
||||
import {isPresent, isBlank, BaseException, stringify} from 'angular2/src/facade/lang';
|
||||
|
||||
import {DomProtoView} from './proto_view';
|
||||
|
||||
import {RenderViewRef, RenderEventDispatcher} from '../../api';
|
||||
import {camelCaseToDashCase} from '../util';
|
||||
|
||||
export function resolveInternalDomView(viewRef: RenderViewRef): DomView {
|
||||
return (<DomViewRef>viewRef)._view;
|
||||
}
|
||||
|
||||
export class DomViewRef extends RenderViewRef {
|
||||
constructor(public _view: DomView) { super(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Const of making objects: http://jsperf.com/instantiate-size-of-object
|
||||
*/
|
||||
export class DomView {
|
||||
hydrated: boolean = false;
|
||||
eventDispatcher: RenderEventDispatcher = null;
|
||||
eventHandlerRemovers: List<Function> = [];
|
||||
|
||||
constructor(public proto: DomProtoView, public boundTextNodes: List<Node>,
|
||||
public boundElements: Element[]) {}
|
||||
|
||||
setElementProperty(elementIndex: number, propertyName: string, value: any) {
|
||||
DOM.setProperty(this.boundElements[elementIndex], propertyName, value);
|
||||
}
|
||||
|
||||
setElementAttribute(elementIndex: number, attributeName: string, value: string) {
|
||||
var element = this.boundElements[elementIndex];
|
||||
var dashCasedAttributeName = camelCaseToDashCase(attributeName);
|
||||
if (isPresent(value)) {
|
||||
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
|
||||
} else {
|
||||
DOM.removeAttribute(element, dashCasedAttributeName);
|
||||
}
|
||||
}
|
||||
|
||||
setElementClass(elementIndex: number, className: string, isAdd: boolean) {
|
||||
var element = this.boundElements[elementIndex];
|
||||
if (isAdd) {
|
||||
DOM.addClass(element, className);
|
||||
} else {
|
||||
DOM.removeClass(element, className);
|
||||
}
|
||||
}
|
||||
|
||||
setElementStyle(elementIndex: number, styleName: string, value: string) {
|
||||
var element = this.boundElements[elementIndex];
|
||||
var dashCasedStyleName = camelCaseToDashCase(styleName);
|
||||
if (isPresent(value)) {
|
||||
DOM.setStyle(element, dashCasedStyleName, stringify(value));
|
||||
} else {
|
||||
DOM.removeStyle(element, dashCasedStyleName);
|
||||
}
|
||||
}
|
||||
|
||||
invokeElementMethod(elementIndex: number, methodName: string, args: List<any>) {
|
||||
var element = this.boundElements[elementIndex];
|
||||
DOM.invoke(element, methodName, args);
|
||||
}
|
||||
|
||||
setText(textIndex: number, value: string) { DOM.setText(this.boundTextNodes[textIndex], value); }
|
||||
|
||||
dispatchEvent(elementIndex: number, eventName: string, event: Event): boolean {
|
||||
var allowDefaultBehavior = true;
|
||||
if (isPresent(this.eventDispatcher)) {
|
||||
var evalLocals = new Map();
|
||||
evalLocals.set('$event', event);
|
||||
// TODO(tbosch): reenable this when we are parsing element properties
|
||||
// out of action expressions
|
||||
// var localValues = this.proto.elementBinders[elementIndex].eventLocals.eval(null, new
|
||||
// Locals(null, evalLocals));
|
||||
// this.eventDispatcher.dispatchEvent(elementIndex, eventName, localValues);
|
||||
allowDefaultBehavior =
|
||||
this.eventDispatcher.dispatchRenderEvent(elementIndex, eventName, evalLocals);
|
||||
if (!allowDefaultBehavior) {
|
||||
DOM.preventDefault(event);
|
||||
}
|
||||
}
|
||||
return allowDefaultBehavior;
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import {ListWrapper, MapWrapper, List} from 'angular2/src/facade/collection';
|
||||
|
||||
import * as viewModule from './view';
|
||||
|
||||
export class DomViewContainer {
|
||||
// The order in this list matches the DOM order.
|
||||
views: List<viewModule.DomView> = [];
|
||||
}
|
13
modules/angular2/src/core/render/render.ts
Normal file
13
modules/angular2/src/core/render/render.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* This module provides advanced support for extending dom strategy.
|
||||
*/
|
||||
|
||||
export * from './dom/compiler/view_loader';
|
||||
export * from './dom/view/shared_styles_host';
|
||||
export * from './dom/compiler/compiler';
|
||||
export * from './dom/dom_renderer';
|
||||
export * from './dom/dom_tokens';
|
||||
export * from './dom/template_cloner';
|
||||
export * from './api';
|
5
modules/angular2/src/core/render/xhr.ts
Normal file
5
modules/angular2/src/core/render/xhr.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {Promise} from 'angular2/src/facade/async';
|
||||
|
||||
export class XHR {
|
||||
get(url: string): Promise<string> { return null; }
|
||||
}
|
14
modules/angular2/src/core/render/xhr_impl.dart
Normal file
14
modules/angular2/src/core/render/xhr_impl.dart
Normal file
@ -0,0 +1,14 @@
|
||||
library angular2.src.services.xhr_impl;
|
||||
|
||||
import 'dart:async' show Future;
|
||||
import 'dart:html' show HttpRequest;
|
||||
import 'package:angular2/di.dart';
|
||||
import './xhr.dart' show XHR;
|
||||
|
||||
@Injectable()
|
||||
class XHRImpl extends XHR {
|
||||
Future<String> get(String url) {
|
||||
return HttpRequest.request(url).then((HttpRequest req) => req.responseText,
|
||||
onError: (_) => new Future.error('Failed to load $url'));
|
||||
}
|
||||
}
|
41
modules/angular2/src/core/render/xhr_impl.ts
Normal file
41
modules/angular2/src/core/render/xhr_impl.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {Promise, PromiseWrapper, PromiseCompleter} from 'angular2/src/facade/async';
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
import {XHR} from './xhr';
|
||||
|
||||
@Injectable()
|
||||
export class XHRImpl extends XHR {
|
||||
get(url: string): Promise<string> {
|
||||
var completer: PromiseCompleter < string >= PromiseWrapper.completer();
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, true);
|
||||
xhr.responseType = 'text';
|
||||
|
||||
xhr.onload = function() {
|
||||
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
|
||||
// response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
|
||||
var response = isPresent(xhr.response) ? xhr.response : xhr.responseText;
|
||||
|
||||
// normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
|
||||
var status = xhr.status === 1223 ? 204 : xhr.status;
|
||||
|
||||
// fix status code when it is 0 (0 status is undocumented).
|
||||
// Occurs when accessing file resources or on Android 4.1 stock browser
|
||||
// while retrieving files from application cache.
|
||||
if (status === 0) {
|
||||
status = response ? 200 : 0;
|
||||
}
|
||||
|
||||
if (200 <= status && status <= 300) {
|
||||
completer.resolve(response);
|
||||
} else {
|
||||
completer.reject(`Failed to load ${url}`, null);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() { completer.reject(`Failed to load ${url}`, null); };
|
||||
|
||||
xhr.send();
|
||||
return completer.promise;
|
||||
}
|
||||
}
|
105
modules/angular2/src/core/render/xhr_mock.ts
Normal file
105
modules/angular2/src/core/render/xhr_mock.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import {XHR} from 'angular2/src/render/xhr';
|
||||
import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection';
|
||||
import {isBlank, isPresent, normalizeBlank, BaseException} from 'angular2/src/facade/lang';
|
||||
import {PromiseCompleter, PromiseWrapper, Promise} from 'angular2/src/facade/async';
|
||||
|
||||
export class MockXHR extends XHR {
|
||||
private _expectations: List<_Expectation>;
|
||||
private _definitions: Map<string, string>;
|
||||
private _requests: List<_PendingRequest>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._expectations = [];
|
||||
this._definitions = new Map();
|
||||
this._requests = [];
|
||||
}
|
||||
|
||||
get(url: string): Promise<string> {
|
||||
var request = new _PendingRequest(url);
|
||||
this._requests.push(request);
|
||||
return request.getPromise();
|
||||
}
|
||||
|
||||
expect(url: string, response: string) {
|
||||
var expectation = new _Expectation(url, response);
|
||||
this._expectations.push(expectation);
|
||||
}
|
||||
|
||||
when(url: string, response: string) { this._definitions.set(url, response); }
|
||||
|
||||
flush() {
|
||||
if (this._requests.length === 0) {
|
||||
throw new BaseException('No pending requests to flush');
|
||||
}
|
||||
|
||||
do {
|
||||
var request = ListWrapper.removeAt(this._requests, 0);
|
||||
this._processRequest(request);
|
||||
} while (this._requests.length > 0);
|
||||
|
||||
this.verifyNoOustandingExpectations();
|
||||
}
|
||||
|
||||
verifyNoOustandingExpectations() {
|
||||
if (this._expectations.length === 0) return;
|
||||
|
||||
var urls = [];
|
||||
for (var i = 0; i < this._expectations.length; i++) {
|
||||
var expectation = this._expectations[i];
|
||||
urls.push(expectation.url);
|
||||
}
|
||||
|
||||
throw new BaseException(`Unsatisfied requests: ${ListWrapper.join(urls, ', ')}`);
|
||||
}
|
||||
|
||||
private _processRequest(request: _PendingRequest) {
|
||||
var url = request.url;
|
||||
|
||||
if (this._expectations.length > 0) {
|
||||
var expectation = this._expectations[0];
|
||||
if (expectation.url == url) {
|
||||
ListWrapper.remove(this._expectations, expectation);
|
||||
request.complete(expectation.response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._definitions.has(url)) {
|
||||
var response = this._definitions.get(url);
|
||||
request.complete(normalizeBlank(response));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new BaseException(`Unexpected request ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
class _PendingRequest {
|
||||
url: string;
|
||||
completer: PromiseCompleter<string>;
|
||||
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.completer = PromiseWrapper.completer();
|
||||
}
|
||||
|
||||
complete(response: string) {
|
||||
if (isBlank(response)) {
|
||||
this.completer.reject(`Failed to load ${this.url}`, null);
|
||||
} else {
|
||||
this.completer.resolve(response);
|
||||
}
|
||||
}
|
||||
|
||||
getPromise(): Promise<string> { return this.completer.promise; }
|
||||
}
|
||||
|
||||
class _Expectation {
|
||||
url: string;
|
||||
response: string;
|
||||
constructor(url: string, response: string) {
|
||||
this.url = url;
|
||||
this.response = response;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user