import { Binding, BindingExpression } from 'aurelia-binding'; import { Container, resolver } from 'aurelia-dependency-injection'; import { DOM } from 'aurelia-pal'; import { LetExpression } from './binding-language'; import { CompositionTransaction } from './composition-transaction'; import { Controller } from './controller'; import { ElementEvents } from './element-events'; import { BehaviorInstruction, TargetInstruction, ViewCreateInstruction } from './instructions'; import { PassThroughSlot, ShadowSlot } from './shadow-dom'; import { ConstructableResourceTarget, InterpolationNode } from './type-extension'; import { View } from './view'; import { ViewResources } from './view-resources'; import { ViewSlot } from './view-slot'; const $resolver = resolver as any; @$resolver class ProviderResolver { get(container, key) { let id = (key as ConstructableResourceTarget).__providerId__; return id in container ? container[id] : (container[id] = container.invoke(key)); } } let providerResolverInstance = new ProviderResolver(); function elementContainerGet(key) { if (key === DOM.Element) { return this.element; } if (key === BoundViewFactory) { if (this.boundViewFactory) { return this.boundViewFactory; } let factory = this.instruction.viewFactory; let partReplacements = this.partReplacements; if (partReplacements) { factory = partReplacements[factory.part] || factory; } this.boundViewFactory = new BoundViewFactory(this, factory, partReplacements); return this.boundViewFactory; } if (key === ViewSlot) { if (this.viewSlot === undefined) { this.viewSlot = new ViewSlot(this.element, this.instruction.anchorIsContainer); this.element.isContentProjectionSource = this.instruction.lifting; this.children.push(this.viewSlot); } return this.viewSlot; } if (key === ElementEvents) { return this.elementEvents || (this.elementEvents = new ElementEvents(this.element)); } if (key === CompositionTransaction) { return this.compositionTransaction || (this.compositionTransaction = this.parent.get(key)); } if (key === ViewResources) { return this.viewResources; } if (key === TargetInstruction) { return this.instruction; } return this.superGet(key); } function createElementContainer(parent, element, instruction, children, partReplacements, resources) { let container = parent.createChild(); let providers; let i; container.element = element; container.instruction = instruction; container.children = children; container.viewResources = resources; container.partReplacements = partReplacements; providers = instruction.providers; i = providers.length; while (i--) { container._resolvers.set(providers[i], providerResolverInstance); } container.superGet = container.get; container.get = elementContainerGet; return container; } function hasAttribute(name) { return this._element.hasAttribute(name); } function getAttribute(name) { return this._element.getAttribute(name); } function setAttribute(name, value) { this._element.setAttribute(name, value); } function makeElementIntoAnchor(element, elementInstruction) { let anchor = DOM.createComment('anchor') as Comment & Pick & { contentElement: Node; _element: Element }; if (elementInstruction) { let firstChild = element.firstChild; if (firstChild && firstChild.tagName === 'AU-CONTENT') { anchor.contentElement = firstChild; } anchor._element = element; anchor.hasAttribute = hasAttribute; anchor.getAttribute = getAttribute; anchor.setAttribute = setAttribute; } DOM.replaceNode(anchor, element); return anchor; } /** * @param {Container[]} containers * @param {Element} element * @param {TargetInstruction} instruction * @param {Controller[]} controllers * @param {Binding[]} bindings * @param {ViewNode[]} children * @param {Record} shadowSlots * @param {Record} partReplacements * @param {ViewResources} resources */ function applyInstructions( containers: Record, element: Element, instruction: TargetInstruction, controllers: Controller[], bindings: Binding[], children: View[], shadowSlots: Record, partReplacements: Record, resources: ViewResources ): void { let behaviorInstructions = instruction.behaviorInstructions; let expressions = instruction.expressions as BindingExpression[]; let elementContainer; let i; let ii; let current; let instance; if (instruction.contentExpression) { bindings.push(instruction.contentExpression.createBinding(element.nextSibling)); (element.nextSibling as InterpolationNode).auInterpolationTarget = true; element.parentNode.removeChild(element); return; } if (instruction.shadowSlot) { let commentAnchor = DOM.createComment('slot'); let slot; if (instruction.slotDestination) { slot = new PassThroughSlot(commentAnchor, instruction.slotName, instruction.slotDestination, instruction.slotFallbackFactory); } else { slot = new ShadowSlot(commentAnchor, instruction.slotName, instruction.slotFallbackFactory); } DOM.replaceNode(commentAnchor, element); shadowSlots[instruction.slotName] = slot; controllers.push(slot); return; } if (instruction.letElement) { for (i = 0, ii = expressions.length; i < ii; ++i) { bindings.push((expressions[i] as LetExpression).createBinding()); } element.parentNode.removeChild(element); return; } if (behaviorInstructions.length) { if (!instruction.anchorIsContainer) { element = makeElementIntoAnchor(element, instruction.elementInstruction) as unknown as Element; } containers[instruction.injectorId] = elementContainer = createElementContainer( containers[instruction.parentInjectorId], element, instruction, children, partReplacements, resources ); for (i = 0, ii = behaviorInstructions.length; i < ii; ++i) { current = behaviorInstructions[i]; instance = current.type.create(elementContainer, current, element, bindings); controllers.push(instance); } } for (i = 0, ii = expressions.length; i < ii; ++i) { bindings.push(expressions[i].createBinding(element)); } } function styleStringToObject(style, target?) { let attributes = style.split(';'); let firstIndexOfColon; let i; let current; let key; let value; target = target || {}; for (i = 0; i < attributes.length; i++) { current = attributes[i]; firstIndexOfColon = current.indexOf(':'); key = current.substring(0, firstIndexOfColon).trim(); value = current.substring(firstIndexOfColon + 1).trim(); target[key] = value; } return target; } function styleObjectToString(obj) { let result = ''; for (let key in obj) { result += key + ':' + obj[key] + ';'; } return result; } function applySurrogateInstruction(container, element, instruction, controllers: Controller[], bindings: Binding[], children: View[]) { let behaviorInstructions = instruction.behaviorInstructions; let expressions = instruction.expressions; let providers = instruction.providers; let values = instruction.values; let i; let ii; let current; let instance; let currentAttributeValue; i = providers.length; while (i--) { container._resolvers.set(providers[i], providerResolverInstance); } //apply surrogate attributes for (let key in values) { currentAttributeValue = element.getAttribute(key); if (currentAttributeValue) { if (key === 'class') { //merge the surrogate classes element.setAttribute('class', currentAttributeValue + ' ' + values[key]); } else if (key === 'style') { //merge the surrogate styles let styleObject = styleStringToObject(values[key]); styleStringToObject(currentAttributeValue, styleObject); element.setAttribute('style', styleObjectToString(styleObject)); } //otherwise, do not overwrite the consumer's attribute } else { //copy the surrogate attribute element.setAttribute(key, values[key]); } } //apply surrogate behaviors if (behaviorInstructions.length) { for (i = 0, ii = behaviorInstructions.length; i < ii; ++i) { current = behaviorInstructions[i]; instance = current.type.create(container, current, element, bindings); if (instance.contentView) { children.push(instance.contentView); } controllers.push(instance); } } //apply surrogate bindings for (i = 0, ii = expressions.length; i < ii; ++i) { bindings.push(expressions[i].createBinding(element)); } } /** * A factory capable of creating View instances, bound to a location within another view hierarchy. */ export class BoundViewFactory { /** @internal */ parentContainer: Container; viewFactory: ViewFactory; /** @internal */ factoryCreateInstruction: { partReplacements: Object; }; /** * Creates an instance of BoundViewFactory. * @param parentContainer The parent DI container. * @param viewFactory The internal unbound factory. * @param partReplacements Part replacement overrides for the internal factory. */ constructor(parentContainer: Container, viewFactory: ViewFactory, partReplacements?: Object) { this.parentContainer = parentContainer; this.viewFactory = viewFactory; this.factoryCreateInstruction = { partReplacements: partReplacements }; //This is referenced internally in the controller's bind method. } /** * Creates a view or returns one from the internal cache, if available. * @return The created view. */ create(): View { let view = this.viewFactory.create(this.parentContainer.createChild(), this.factoryCreateInstruction); view._isUserControlled = true; return view; } /** * Indicates whether this factory is currently using caching. */ get isCaching() { return this.viewFactory.isCaching; } /** * Sets the cache size for this factory. * @param size The number of views to cache or "*" to cache all. * @param doNotOverrideIfAlreadySet Indicates that setting the cache should not override the setting if previously set. */ setCacheSize(size: number | string, doNotOverrideIfAlreadySet: boolean): void { this.viewFactory.setCacheSize(size, doNotOverrideIfAlreadySet); } /** * Gets a cached view if available... * @return A cached view or null if one isn't available. */ getCachedView(): View { return this.viewFactory.getCachedView(); } /** * Returns a view to the cache. * @param view The view to return to the cache if space is available. */ returnViewToCache(view: View): void { this.viewFactory.returnViewToCache(view); } } /** * A factory capable of creating View instances. */ export class ViewFactory { /** * Indicates whether this factory is currently using caching. */ isCaching = false; template: DocumentFragment; instructions: Object; resources: ViewResources; cacheSize: number; cache: any; surrogateInstruction: any; part: any; /** * Creates an instance of ViewFactory. * @param template The document fragment that serves as a template for the view to be created. * @param instructions The instructions to be applied ot the template during the creation of a view. * @param resources The resources used to compile this factory. */ constructor(template: DocumentFragment, instructions: Object, resources: ViewResources) { this.template = template; this.instructions = instructions; this.resources = resources; this.cacheSize = -1; this.cache = null; } /** * Sets the cache size for this factory. * @param size The number of views to cache or "*" to cache all. * @param doNotOverrideIfAlreadySet Indicates that setting the cache should not override the setting if previously set. */ setCacheSize(size: number | string, doNotOverrideIfAlreadySet?: boolean): void { if (size) { if (size === '*') { size = Number.MAX_VALUE; } else if (typeof size === 'string') { size = parseInt(size, 10); } } if (this.cacheSize === -1 || !doNotOverrideIfAlreadySet) { this.cacheSize = Number(size); } if (this.cacheSize > 0) { this.cache = []; } else { this.cache = null; } this.isCaching = this.cacheSize > 0; } /** * Gets a cached view if available... * @return A cached view or null if one isn't available. */ getCachedView(): View { return this.cache !== null ? (this.cache.pop() || null) : null; } /** * Returns a view to the cache. * @param view The view to return to the cache if space is available. */ returnViewToCache(view: View): void { if (view.isAttached) { view.detached(); } if (view.isBound) { view.unbind(); } if (this.cache !== null && this.cache.length < this.cacheSize) { view.fromCache = true; this.cache.push(view); } } /** * Creates a view or returns one from the internal cache, if available. * @param container The container to create the view from. * @param createInstruction The instruction used to customize view creation. * @param element The custom element that hosts the view. * @return The created view. */ create(container: Container, createInstruction?: ViewCreateInstruction, element?: Element): View { createInstruction = createInstruction || BehaviorInstruction.normal; let cachedView = this.getCachedView(); if (cachedView !== null) { return cachedView; } let fragment = createInstruction.enhance ? this.template : this.template.cloneNode(true) as DocumentFragment; let instructables = fragment.querySelectorAll('.au-target'); let instructions = this.instructions; let resources = this.resources; let controllers: Controller[] = []; let bindings: Binding[] = []; let children: View[] = []; let shadowSlots = Object.create(null); let containers = { root: container }; let partReplacements = createInstruction.partReplacements as Record; let i; let ii; let view: View; let instructable; let instruction; this.resources._invokeHook('beforeCreate', this, container, fragment, createInstruction); if (element && this.surrogateInstruction !== null) { applySurrogateInstruction(container, element, this.surrogateInstruction, controllers, bindings, children); } // todo: better typings in view & view-factory to remove the cast if (createInstruction.enhance && (fragment as unknown as Element).hasAttribute('au-target-id')) { instructable = fragment; instruction = instructions[instructable.getAttribute('au-target-id')]; applyInstructions(containers, instructable, instruction, controllers, bindings, children, shadowSlots, partReplacements, resources); } for (i = 0, ii = instructables.length; i < ii; ++i) { instructable = instructables[i]; instruction = instructions[instructable.getAttribute('au-target-id')]; applyInstructions(containers, instructable, instruction, controllers, bindings, children, shadowSlots, partReplacements, resources); } view = new View(container, this, fragment, controllers, bindings, children, shadowSlots); //if iniated by an element behavior, let the behavior trigger this callback once it's done creating the element if (!createInstruction.initiatedByBehavior) { view.created(); } this.resources._invokeHook('afterCreate', view); return view; } }