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