Now, the whole document is templateable
[knw_133/js/MVSync/.git] / MVSync.js
1 (function(){
2 "use strict";
3
4 window['model'] = window['model'] || {};
5
6 /** @type {function(Object,function(Object))} */
7 Object.observe;
8 /** @type {function(Object,function(Object))} */
9 Object.unobserve;
10
11 var templates = {};
12 var requiredTemplates = {};
13
14 Object.observe(requiredTemplates,function(changes){
15   changes.forEach(function(ch){
16     if(ch.type=="add")
17       loadTemplate(ch.name);
18   });
19 });
20
21 /**
22  * @constructor
23  */
24 function Template(template){
25   template.removeAttribute("data-template");
26   var attrName = template.templateName;
27   template.template = this;
28   /**
29    * @constructor
30    */
31   function Instance(scope){
32     var sc = this;
33
34     Object.defineProperty(scope,"this",{
35       configurable: true,
36       writable: true,
37       enumerable: false,
38       value: scope
39     });
40
41     var mapping = {};
42     var getterCache = {};
43     var getterCacheByProperty = {};
44     var setterCache = {};
45     var setterCacheByProperty = {};
46
47     this.containingTemplateInstances = [];
48
49     function evaluateExpression(expr){
50       try {
51         return (new Function("v","return v."+expr+";"))(scope);
52       } catch(e) {
53         return null;
54       }
55     }
56     function update(name,modelAction){
57       if(name in mapping)
58         for(var i in mapping[name]){(function(){
59           var b = mapping[name][i];
60           var something = null;
61           if(b.expr)
62             something = evaluateExpression(b.expr);
63           var value = null;
64           switch(b.type){
65             case "getter": {
66               var cache
67                 =  getterCache[name+b.expr]
68                 =  getterCacheByProperty[b.which]
69                 =  getterCache[name+b.expr]
70                 || getterCacheByProperty[b.which]
71                 || {};
72               if(setterCacheByProperty[b.which]){
73                 cache.relatedSetter = setterCacheByProperty[b.which];
74               }
75               var update = function(value){
76                 cache.lastValue = value;
77                 while(cache.reqList.length){
78                   var name = cache.reqList.pop();
79                   (new Function("s","n","v","s[n]"+b.expr+"=v;")).call(scope,scope,name,value);
80                 }
81               }
82               cache.reqList = cache.reqList || [];
83               if(!cache.reqList.length){
84                 value = something();
85                 if(value instanceof Function){
86                   value(update); // value is a function which takes a callback function
87                 }else{
88                   update(value);
89                 }
90               }
91               cache.reqList.push(b.which);
92             } break;
93             case "setter": {
94               var scache
95                 =  setterCache[name+b.expr]
96                 =  setterCacheByProperty[b.which]
97                 =  setterCache[name+b.expr]
98                 || setterCacheByProperty[b.which]
99                 || {};
100               var gcache
101                 =  getterCacheByProperty[b.which]
102                 || getterCacheByProperty[b.which]
103                 || {};
104               gcache.relatedSetter = scache;
105               scache.func = something;
106             } break;
107             case "value": {
108               var value = something;
109               if(
110                 !(name in getterCacheByProperty) ||
111                 getterCacheByProperty[name].lastValue != value
112               ){ // value wasn't changed by a getter
113                 var setter = null;
114                 if(name in getterCacheByProperty){
115                   setter = getterCacheByProperty[name].relatedSetter.func;
116                   delete getterCacheByProperty[name].lastValue;
117                 }
118                 if( !setter && (name in setterCacheByProperty) )
119                   setter = setterCacheByProperty[name].func;
120                 if(setter){
121                   var callback = setter(value);
122                   if(callback)
123                     callback();
124                 }
125               }
126               if(b.what=="attribute"){
127                 setAttr(b.element,b.which,value);
128               }else if(b.what=="content"){
129                 setContent(b.element,value);
130               }
131             } break;
132             case "map": {
133               b.update();
134             } break;
135           }
136         })();}
137     }
138
139     this.root = template.cloneNode(true);
140     this.root.templateInstance = this;
141     this.root.classList.add(attrName);
142
143     function clearContent(el){
144       while(el.childNodes.length){
145         var e = el.childNodes[el.childNodes.length-1];
146         if(e.templateInstance)
147           e.templateInstance._cleanup();
148         el.removeChild(e);
149       }
150       if("_content" in el)
151         delete el._content;
152     }
153     this._cleanup = function(){
154       Object.unobserve(this.observer.obj,this.observer.func);
155       for(var i=0;i<this.containingTemplateInstances.length;i++){
156         var templateInstance = this.containingTemplateInstances[i];
157         templateInstance._cleanup();
158       }
159     }
160
161     function setAttr(element,name,value){
162       if(name in element)
163         element[name] = value;
164       else
165         element.setAttribute(name,value);
166     }
167
168     function setContent(element,value){
169       clearContent(element);
170       if(value instanceof Node){
171         var node = value.cloneNode(true);
172         setup(node);
173         element.appendChild(node);
174       }else{
175         element.appendChild(document.createTextNode(value||''));
176       }
177     }
178
179     function setup(e){
180       var bind = e.getAttribute("data-bind");
181       if(bind){
182         bind = bind.split("¦");
183         for(var i=0;i<bind.length;i++){
184           var b = bind[i].split(":");
185           var expr = b[1];
186           var name = expr.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/)[1];
187           var attr = b[0];
188           var value = evaluateExpression(expr);
189           setAttr(e,attr,value);
190           mapping[name] = mapping[name] || [];
191           mapping[name].push({
192             which: attr,
193             what: "attribute",
194             type: "value",
195             expr: expr,
196             element: e
197           });
198         }
199       }
200       var content = e.getAttribute("data-content");
201       if(content){
202         var expr = content.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/);
203         var name = expr[1];
204         expr = content;
205         var value = evaluateExpression(expr);
206         setContent(e,value);
207         mapping[name] = mapping[name] || [];
208         mapping[name].push({
209           what: "content",
210           type: "value",
211           expr: expr,
212           element: e
213         });
214       }
215       var getter = e.getAttribute("data-getter");
216       if(getter){
217         getter = getter.split("¦");
218         for(var i=0;i<getter.length;i++){
219           var g = getter[i].split(":");
220           var expr = g[1];
221           var name = expr.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/)[1];
222           var attr = g[0];
223           mapping[name] = mapping[name] || [];
224           mapping[name].push({
225             which: attr,
226             type: "getter",
227             expr: expr,
228             element: e
229           });
230         }
231       }
232       var setter = e.getAttribute("data-setter");
233       if(setter){
234         setter = setter.split("¦");
235         for(var i=0;i<setter.length;i++){
236           var s = setter[i].split(":");
237           var expr = s[1];
238           var name = expr.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/)[1];
239           var attr = s[0];
240           mapping[name] = mapping[name] || [];
241           mapping[name].push({
242             which: attr,
243             type: "setter",
244             expr: expr,
245             element: e
246           });
247         }
248       }
249       var updateScope = function(attr){
250         var value = e.getAttribute(attr);
251         for(var name in mapping){
252           var maps = mapping[name];
253           for(var i in maps){
254             var map = maps[i];
255             if(map.type!="value"||map.which!=attr)
256               continue;
257             (new Function("s","v","if(s."+map.expr+".toString()!=v)s."+map.expr+"=Object(s."+map.expr+").constructor(v);")).call(scope,scope,value);
258           }
259         }
260       };
261       var observer = new MutationObserver(function(mutations){
262         mutations.forEach(function(mutation){;
263           updateScope(mutation.attributeName);
264         });
265       });
266       observer.observe( e, /** @type {!MutationObserverInit} */ ({ attributes: true }) );
267       if("value" in e)
268         e.addEventListener("change",function(){
269           e.setAttribute("value",e.value);
270         });
271
272       var map = e.getAttribute("data-map");
273       if(map){
274         var v = map.split(":");
275         var expr = v[0].match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/);
276         var name = expr[1];
277         expr=expr[2];
278         var setup_mapping = function(){
279           if(!(v[1] in templates)){
280             ( requiredTemplates[v[1]] = requiredTemplates[v[1]] || [] ).push( setup_mapping );
281             return;
282           }
283           var target = scope[name];
284           if(target)
285             target = Object((new Function("v","return v"+expr+";"))(target));
286           if(!target)
287             target=[];
288 //          clearContent(e);
289           if(!("length" in target))
290             target=[target];
291           e._content = e._content || {
292             subScopes: [],
293             subScopeInfos: []
294           };
295           Object.observe(target,function(changes){
296             syncLists(e._content,target,e,templates[v[1]]);
297           });
298           syncLists(e._content,target,e,templates[v[1]]);
299         };
300         ( mapping[v[0]] = mapping[v[0]] || [] ).push({
301           "type": "map",
302           "update": setup_mapping
303         });
304         setup_mapping();
305       }
306
307       if(!map)
308         for(var i=0;i<e.children.length;i++)
309           setup(e.children[i]);
310     }
311
312     function syncLists(contentDatas,b,e,t){
313       var a = contentDatas.subScopes;
314       var d = contentDatas.subScopeInfos;
315       for(var i=a.length;i--;){ // remove elements / objects
316         if(b.indexOf(a[i])!=-1)
317           continue;
318         if(d[i].element.parentNode)
319           d[i].element.parentNode.removeChild(d[i].element);
320         a.splice(i,1);
321         d.splice(i,1);
322       }
323       for(var i=0;i<b.length;i++){ // add elements / objects
324         var j = a.indexOf(b[i]);
325         if(j!=-1){
326           d[j].index = i;
327         }else{
328           var newInfo = {
329             element: t.instance(b[i]),
330             index: i
331           };
332           if(!d.length){
333             e.appendChild(newInfo.element);
334           }else{
335             var last = d[d.length-1].element;
336             if(last.parentNode==e){
337               e.insertBefore(newInfo.element,last.nextSibling);
338             }else{
339               e.appendChild(newInfo.element);
340             }
341           }
342           a.push(b[i]);
343           d.push(newInfo);
344         }
345       }
346       for(var i=0;i<d.length;i++){ // move elements / objects to desired index
347         var x = d[i];
348         if(x.index==i)
349           continue;
350         var ae = x.element;
351         var be = d[x.index].element;
352         arraySwapValues(d,i,x.index);
353         arraySwapValues(a,i,x.index);
354         var ap = ae.parentNode;
355         var bp = be.parentNode;
356         if(ap&&bp){
357           var an = ae.nextSibling;
358           var bn = be.nextSibling;
359           bp.insertBefore(ae,bn);
360           ap.insertBefore(be,an);
361         }
362       }
363     }
364
365     setup(this.root);
366
367     var observerFunc = function(changes){
368       changes.forEach(function(ch){
369         update(ch.name,ch.type);
370       });
371     }
372     var observerObj = Object.observe(scope,observerFunc);
373     sc.observer = {
374       obj: observerObj,
375       func: observerFunc
376     };
377
378     for(var i in scope){
379       update(i,"add");
380     }
381
382   };
383   this.instance = function(scope){
384     var instance = new Instance(scope);
385     return instance.root;
386   };
387 }
388
389 function arraySwapValues(a,i,j){
390   a[i]=[a[j],a[j]=a[i]][0];
391 }
392
393 function compileTemplate(e){
394   var name = e.getAttribute("data-template");
395   if(e==document.documentElement)
396     name = "root";
397   if(e.parentNode)
398     e.parentNode.removeChild(e);
399   e.templateName = name;
400   var t = templates[name] = new Template(e);
401   if(name in requiredTemplates){
402     while(requiredTemplates[name].length){
403       requiredTemplates[name].pop()();
404     }
405     delete requiredTemplates[name];
406   }
407   return t;
408 }
409
410 function compileTemplates( templates ){
411   if( "querySelectorAll" in templates )
412     templates = templates.querySelectorAll("[data-template]");
413   for(var i=0;i<templates.length;i++){
414     compileTemplate(templates[i]);
415   }
416 }
417
418 var base = "/";
419
420 function loadTemplate(name){
421   var url = base+"templates/"+name+".html";
422   var xhr = new XMLHttpRequest();
423   xhr.open("GET",url,true);
424   xhr.onload = function(e){
425     var result = null;//this.responseXML;
426     if(!result){
427       result = document.createDocumentFragment();
428       var div = document.createElement("div");
429       div.innerHTML = this.responseText;
430       result.appendChild(div);
431     }
432     compileTemplates(result);
433   };
434   xhr.send();
435 }
436
437 addEventListener("load",function(){
438   if(window['templateRoot']){
439     base = window['templateRoot'] + "/";
440   }else if(document.querySelector("[data-template-root]")){
441     base = document.querySelector("[data-template-root]").getAttribute("data-template-root") + "/";
442   }
443   compileTemplates(document);
444   var t = compileTemplate(document.documentElement);
445   document.appendChild(t.instance(model));
446 });
447
448 })();