Fragment.js 5.0 KB

1234567891011121314151617181920212223242526272829
  1. export default class Fragment{constructor(element){this._element=element;this._elementsById=new Map();}
  2. element(){return this._element;}
  3. $(elementId){return this._elementsById.get(elementId);}
  4. static build(strings,...values){return Fragment._render(Fragment._template(strings),values);}
  5. static cached(strings,...values){let template=_templateCache.get(strings);if(!template){template=Fragment._template(strings);_templateCache.set(strings,template);}
  6. return Fragment._render(template,values);}
  7. static _template(strings){let html='';let insideText=true;for(let i=0;i<strings.length-1;i++){html+=strings[i];const close=strings[i].lastIndexOf('>');const open=strings[i].indexOf('<',close+1);if(close!==-1&&open===-1){insideText=true;}else if(open!==-1){insideText=false;}
  8. html+=insideText?Fragment._textMarker:Fragment._attributeMarker(i);}
  9. html+=strings[strings.length-1];const template=window.document.createElement('template');template.innerHTML=html;const walker=template.ownerDocument.createTreeWalker(template.content,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT,null,false);let valueIndex=0;const emptyTextNodes=[];const binds=[];const nodesToMark=[];while(walker.nextNode()){const node=walker.currentNode;if(node.nodeType===Node.ELEMENT_NODE&&node.hasAttributes()){if(node.hasAttribute('$')){nodesToMark.push(node);binds.push({elementId:node.getAttribute('$')});node.removeAttribute('$');}
  10. const attributesToRemove=[];for(let i=0;i<node.attributes.length;i++){const name=node.attributes[i].name;if(!_attributeMarkerRegex.test(name)&&!_attributeMarkerRegex.test(node.attributes[i].value)){continue;}
  11. attributesToRemove.push(name);nodesToMark.push(node);const bind={attr:{index:valueIndex}};bind.attr.names=name.split(_attributeMarkerRegex);valueIndex+=bind.attr.names.length-1;bind.attr.values=node.attributes[i].value.split(_attributeMarkerRegex);valueIndex+=bind.attr.values.length-1;binds.push(bind);}
  12. for(let i=0;i<attributesToRemove.length;i++){node.removeAttribute(attributesToRemove[i]);}}
  13. if(node.nodeType===Node.TEXT_NODE&&node.data.indexOf(Fragment._textMarker)!==-1){const texts=node.data.split(_textMarkerRegex);node.data=texts[texts.length-1];for(let i=0;i<texts.length-1;i++){if(texts[i]){node.parentNode.insertBefore(createTextNode(texts[i]),node);}
  14. const nodeToReplace=createElement('span');nodesToMark.push(nodeToReplace);binds.push({replaceNodeIndex:valueIndex++});node.parentNode.insertBefore(nodeToReplace,node);}}
  15. if(node.nodeType===Node.TEXT_NODE&&(!node.previousSibling||node.previousSibling.nodeType===Node.ELEMENT_NODE)&&(!node.nextSibling||node.nextSibling.nodeType===Node.ELEMENT_NODE)&&/^\s*$/.test(node.data)){emptyTextNodes.push(node);}}
  16. for(let i=0;i<nodesToMark.length;i++){nodesToMark[i].classList.add(_class(i));}
  17. for(const emptyTextNode of emptyTextNodes){emptyTextNode.remove();}
  18. return{template:template,binds:binds};}
  19. static _render(template,values){const content=template.template.ownerDocument.importNode(template.template.content,true);const resultElement=(content.firstChild===content.lastChild?content.firstChild:content);const result=new Fragment(resultElement);const boundElements=[];for(let i=0;i<template.binds.length;i++){const className=_class(i);const element=(content.querySelector('.'+className));element.classList.remove(className);boundElements.push(element);}
  20. for(let bindIndex=0;bindIndex<template.binds.length;bindIndex++){const bind=template.binds[bindIndex];const element=boundElements[bindIndex];if('elementId'in bind){result._elementsById.set((bind.elementId),element);}else if('replaceNodeIndex'in bind){const value=values[(bind.replaceNodeIndex)];element.parentNode.replaceChild(this._nodeForValue(value),element);}else if('attr'in bind){if(bind.attr.names.length===2&&bind.attr.values.length===1&&typeof values[bind.attr.index]==='function'){values[bind.attr.index].call(null,element);}else{let name=bind.attr.names[0];for(let i=1;i<bind.attr.names.length;i++){name+=values[bind.attr.index+i-1];name+=bind.attr.names[i];}
  21. if(name){let value=bind.attr.values[0];for(let i=1;i<bind.attr.values.length;i++){value+=values[bind.attr.index+bind.attr.names.length-1+i-1];value+=bind.attr.values[i];}
  22. element.setAttribute(name,value);}}}else{throw new Error('Unexpected bind');}}
  23. return result;}
  24. static _nodeForValue(value){if(value instanceof Node){return value;}
  25. if(value instanceof Fragment){return value._element;}
  26. if(Array.isArray(value)){const node=createDocumentFragment();for(const v of value){node.appendChild(this._nodeForValue(v));}
  27. return node;}
  28. return createTextNode(''+value);}}
  29. export const _textMarker='{{template-text}}';const _textMarkerRegex=/{{template-text}}/;export const _attributeMarker=index=>'template-attribute'+index;const _attributeMarkerRegex=/template-attribute\d+/;const _class=index=>'template-class-'+index;const _templateCache=new Map();export const html=(strings,...vararg)=>{return Fragment.cached(strings,...vararg).element();};self.UI=self.UI||{};UI=UI||{};UI.Fragment=Fragment;UI.Fragment._textMarker=_textMarker;UI.Fragment._attributeMarker=_attributeMarker;UI.html=html;UI.Fragment._Template;UI.Fragment._Bind;