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