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