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