UNPKG

mframejs

Version:
406 lines (320 loc) 15.1 kB
import { ContainerElements } from '../container/exported'; import { ContainerAttributes } from '../container/exported'; import { Cache } from '../utils/exported'; import { AttributeController } from './attributeController'; import { ElementController } from './elementController'; import { InterpolateController } from './interpolateController'; import { ViewController } from './viewController'; import { IElement, IControllerArray, IBindingContext, ITemplateCache } from '../interface/exported'; import { DOM } from '../utils/exported'; /** * This class have methods to parse the string/template and initiate our custom elements/attributes/binding * */ export class View { /** * creates element and parses template string * just used insternally for creating root * @internal * @param _class - class to use * @param element - element node * @param bindingContext - binding Context * @param templateString - template string * @param viewController - viewController */ public static parseAndCreateElement( _class: IElement, element: Node, bindingContext: IBindingContext, templateString: string, viewController: ViewController ): ElementController { const elementController = new ElementController(bindingContext, element, _class, null, templateString, viewController); const controller = elementController.init(); return controller; } /** * cleans a html template * @param template - template element to clean up */ public static cleanTemplate(template: HTMLElement): void { const loopNodes = (node: Node) => { for (let n = 0; n < node.childNodes.length; n++) { let child = node.childNodes[n]; if (child.nodeType === 8 || (child.nodeType === 3 && !/\S/.test(child.nodeValue))) { // 8 Represents a comment // Represents textual content in an element or attribute, but check if empty node.removeChild(child); n--; } else if (child.nodeType === 1) { // 1 = Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference loopNodes(child); } child = null; } }; loopNodes(template); } /** * creates a template from string markup * @param markup - string markup to add- remember to wrap inside <template> */ public static createTemplate(markup: string): Element { let template: any; if (!Cache.templateMap.has(markup)) { const container = DOM.document.createElement('div'); container.innerHTML = (<any>markup).default || markup; const fragment = container.firstElementChild; if (!(<any>fragment).content) { // ie11 fix (<any>fragment).content = DOM.document.createDocumentFragment(); while (fragment.childNodes[0]) { (<any>fragment).content.appendChild(fragment.childNodes[0]); } } template = DOM.document.createElement('mf-template'); View.cleanTemplate((fragment as any).content); template.appendChild((fragment as any).content); Cache.templateMap.set(markup, template); } else { template = Cache.templateMap.get(markup); } const x = template.cloneNode(true); template = null; return x; } /** * adds template and calls attached * @param template - template to attach * @param toNode - node you want to attach it to * @param controllers - controllers to attach */ public static attachTemplate(template: any, toNode: Element, controllers: IControllerArray): Element[] { const el = []; while (template.firstChild) { el.push(template.firstChild); toNode.appendChild(template.firstChild); } controllers.forEach((x) => { if (x.attached) { x.attached(); } }); return el; } /** * helper for clearing views on array of elements * optional viewController you can pass in too * if you are to lazy for calling viewController.clearView() :-) * @param elements - elements to remove * @param viewController - viewcontrolelrs to remove */ public static clearViews(elements: Element[], viewController?: ViewController) { if (viewController) { viewController.clearView(); } elements.forEach((el) => { // this will traverse down to every child and tell them to detach if (el.parentNode) { el.parentNode.removeChild(el); } }); } /** * parses nodes and adds initiates elements/interpolate and attributes * use cache version when generating many * * @param template - template to use * @param bindingContext - binding context * @param viewController - viewcontroller */ public static parseTemplate(template: Node, bindingContext: IBindingContext, viewController: ViewController): IControllerArray { const templateCache = View.createTemplateCache(template); const controllers = View.parseTemplateCache(template, bindingContext, viewController, templateCache); return controllers; } /** * parses nodes and adds initiates elements/interpolate and attributes * this uses cache to loop, use createTemplateCache to generate cache * * @internal * @param template - template to use * @param bindingContext - bindingcontext to use * @param viewController - viewController * @param cacheX - template cache */ public static parseTemplateCache( template: Node, bindingContext: IBindingContext, viewController: ViewController, cacheX: ITemplateCache[]): IControllerArray { const arr: IControllerArray = []; const cache = cacheX.slice().reverse(); // loops and pushes into our node map/array const loopNodes = function (_node: Node) { // loop children for (let n = 0; n < _node.childNodes.length; n++) { const htmlNode = _node.childNodes[n]; const curcach = cache.pop(); curcach.attributes.forEach((attr) => { let controller: any; let attrNode: any; switch (attr.type) { case 'controller': attrNode = (htmlNode as Element).getAttributeNode(attr.name); controller = new AttributeController(bindingContext, htmlNode, attrNode, attr.container, viewController); arr.push(controller); break; case 'value': attrNode = (htmlNode as Element).getAttributeNode(attr.name); controller = new InterpolateController(bindingContext, attrNode, viewController, true); arr.push(controller); break; case 'attribute': attrNode = (htmlNode as Element).getAttributeNode(attr.name); controller = new AttributeController(bindingContext, htmlNode, attrNode, attr.container, viewController); arr.push(controller); break; } }); switch (curcach.type) { case 'element': const elementController = new ElementController(bindingContext, htmlNode, null, curcach.container, null, viewController); arr.push(elementController); break; case 'text': const interpolateController = new InterpolateController(bindingContext, htmlNode, viewController, false); arr.push(interpolateController); break; default: if (curcach.gotoChild) { loopNodes(htmlNode); } } } }; const tempTemplate = ({ childNodes: [template] } as any); loopNodes(tempTemplate); arr.forEach((instance) => { instance.init(); }); return arr; } /** * parses nodes and creates cache to be used in repeat * @param template - template to use */ public static createTemplateCache(template: Node): ITemplateCache[] { const arr: ITemplateCache[] = []; // loops and pushes into our node map/array const loopNodes = function (_node: Node) { // loop children for (let n = 0; n < _node.childNodes.length; n++) { const currentNode = _node.childNodes[n] as Element; const instance: ITemplateCache = { type: 'blank', attributes: ([] as any), container: null, gotoChild: false }; arr.push(instance); // check controllers first let isControllerAttribute = false; if (currentNode.getAttribute) { // TODO: need to add option to add own here ['repeat.for', 'if.bind'].forEach((attribute: string) => { if (!isControllerAttribute) { // only 1 controller per element const attributeNode = currentNode.getAttributeNode(attribute); if (attributeNode) { isControllerAttribute = true; const customAttribute = ContainerAttributes.findAttribute(attributeNode.name); instance.attributes.push({ type: 'controller', name: attribute, container: customAttribute }); } } }); } if (!isControllerAttribute) { const childAttributes: Attr[] = []; const length = currentNode.attributes && currentNode.attributes.length || 0; for (let i = 0; i < length; i++) { childAttributes.push(currentNode.attributes[i]); } for (let i = 0; i < childAttributes.length; i++) { const attributeNode = childAttributes[i]; let customAttribute = ContainerAttributes.findAttribute(attributeNode.name); if (!customAttribute && attributeNode.name) { customAttribute = ContainerAttributes.findAttribute(attributeNode.name.replace('.bind', '')); if (!customAttribute && attributeNode.name) { customAttribute = ContainerAttributes.findAttribute(View.getAttributeExpression(attributeNode.name)); } } if (customAttribute) { instance.attributes.push({ type: 'attribute', name: attributeNode.name, container: customAttribute }); } else { // attribute value if (attributeNode.value.indexOf('${') !== -1 || attributeNode.value.indexOf('@{') !== -1) { if (attributeNode.name.indexOf('.bind') === -1) { instance.attributes.push({ type: 'value', name: attributeNode.name, container: null }); } } } } } if (!isControllerAttribute) { if (currentNode.nodeType === 1) { const customElement = ContainerElements.findElement((<Element>currentNode).localName); if (customElement) { instance.type = 'element'; instance.container = customElement; } else { if (currentNode.childNodes) { instance.gotoChild = true; loopNodes(currentNode); } } } else { if ((<any>currentNode).textContent) { if ((<any>currentNode).textContent.indexOf('${') !== -1 || (<any>currentNode).textContent.indexOf('@{') !== -1) { instance.type = 'text'; } } } } } }; const tempTemplate = ({ childNodes: [template] } as any); loopNodes(tempTemplate); return arr; } /** * Gets custom attibute search expressions form string * @param stringAttribute - string attibute to check * @internal * * click.trigger === #VARIABLE#.trigger * some-custom-event.bind === #VARIABLE#.bind * style@background-color.bind === style#VARIABLE#.bind * style@background-color === style#VARIABLE# * * NB! I first check for just value, and just value + '.bind' * This is just if 2 two checks fails */ public static getAttributeExpression(stringAttribute: string) { const atIndex = stringAttribute.indexOf('@') !== -1 ? stringAttribute.indexOf('@') : 0; const dotIndex = stringAttribute.lastIndexOf('.') !== -1 ? stringAttribute.lastIndexOf('.') : stringAttribute.length; const toReplace = stringAttribute.substring(atIndex, dotIndex); return stringAttribute.replace(toReplace, '#VARIABLE#'); } }