import { IPlatform, resolve, isString } from '@aurelia/kernel'; import { tcCreateInterface } from './utilities'; import { IDomPlatform } from './interfaces-template-compiler'; /** * Utility that creates a `HTMLTemplateElement` out of string markup or an existing DOM node. * * It is idempotent in the sense that passing in an existing template element will simply return that template element, * so it is always safe to pass in a node without causing unnecessary DOM parsing or template creation. */ export interface ITemplateElementFactory { createTemplate(input: string | Node): HTMLTemplateElement; } export const ITemplateElementFactory = /*@__PURE__*/tcCreateInterface('ITemplateElementFactory', x => x.singleton(TemplateElementFactory)); const markupCache: Record = {}; export class TemplateElementFactory implements ITemplateElementFactory { /** @internal */ private readonly p = resolve(IPlatform) as IDomPlatform; /** @internal */ private _template = this.t(); private t() { return this.p.document.createElement('template'); } public createTemplate(input: string | Node): HTMLTemplateElement { if (isString(input)) { let result = markupCache[input]; if (result === void 0) { const template = this._template; template.innerHTML = input; const node = template.content.firstElementChild; // if the input is either not wrapped in a template or there is more than one node, // return the whole template that wraps it/them (and create a new one for the next input) if (needsWrapping(node)) { this._template = this.t(); result = template; } else { // the node to return is both a template and the only node, so return just the node // and clean up the template for the next input template.content.removeChild(node!); result = node as HTMLTemplateElement; } markupCache[input] = result; } return result.cloneNode(true) as HTMLTemplateElement; } if (input.nodeName !== 'TEMPLATE') { // if we get one node that is not a template, wrap it in one const template = this.t(); template.content.appendChild(input); return template; } // we got a template element, remove it from the DOM if it's present there and don't // do any other processing input.parentNode?.removeChild(input); return input.cloneNode(true) as HTMLTemplateElement; function needsWrapping(node: Element | null | undefined): boolean { if (node == null) return true; if (node.nodeName !== 'TEMPLATE') return true; // At this point the node is a template element. // If the template has meaningful siblings, then it needs wrapping. // low-hanging fruit: check the next element sibling const nextElementSibling = node.nextElementSibling; if (nextElementSibling != null) return true; // check the previous sibling const prevSibling = node.previousSibling; if (prevSibling != null) { switch (prevSibling.nodeType) { // The previous sibling cannot be an element, because the node is the first element in the template. case 3: // Text return prevSibling.textContent!.trim().length > 0; } } // the previous sibling was not meaningful, so check the next sibling const nextSibling = node.nextSibling; if (nextSibling != null) { switch (nextSibling.nodeType) { // element is already checked above case 3: // Text return nextSibling.textContent!.trim().length > 0; } } // neither the previous nor the next sibling was meaningful, hence the template does not need wrapping return false; } } }