_
[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=="style"){
131                 setStyle(b.element,b.which,value);
132               }else if(b.what=="content"){
133                 setContent(b.element,value);
134               }
135             } break;
136             case "map": {
137               b.update();
138             } break;
139           }
140         })();}
141     }
142
143     this.root = (template instanceof HTMLHtmlElement)?template:template.cloneNode(true);
144     this.root.templateInstance = this;
145     this.root.classList.add(attrName);
146
147     function clearContent(el){
148       while(el.childNodes.length){
149         var e = el.childNodes[el.childNodes.length-1];
150         if(e.templateInstance)
151           e.templateInstance._cleanup();
152         el.removeChild(e);
153       }
154       if("_content" in el)
155         delete el._content;
156     }
157     this._cleanup = function(){
158       Object.unobserve(this.observer.obj,this.observer.func);
159       for(var i=0;i<this.containingTemplateInstances.length;i++){
160         var templateInstance = this.containingTemplateInstances[i];
161         templateInstance._cleanup();
162       }
163     }
164
165     function setAttr(element,name,value){
166       if(name in element)
167         element[name] = value;
168       else
169         element.setAttribute(name,value===undefined?"":value);
170     }
171
172     function setStyle(element,name,value){
173       element.style[name] = value===undefined?"":value;
174     }
175
176     function setContent(element,value){
177       clearContent(element);
178       if(value instanceof Node){
179         var node = value.cloneNode(true);
180         setup(node);
181         element.appendChild(node);
182       }else{
183         element.appendChild(document.createTextNode(value||''));
184       }
185     }
186
187     function setup(e){
188       var bind = e.getAttribute("data-bind");
189       if(bind){
190         bind = bind.split("¦");
191         for(var i=0;i<bind.length;i++){
192           var b = bind[i].split(":");
193           var expr = b[1];
194           var name = expr.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/)[1];
195           var style = b[0].substr(0,1) == "@";
196           var attr = style?b[0].substr(1):b[0];
197           var value = evaluateExpression(expr);
198           if(style)
199             setStyle(e,attr,value);
200           else
201             setAttr(e,attr,value);
202           mapping[name] = mapping[name] || [];
203           mapping[name].push({
204             which: attr,
205             what: style?"style":"attribute",
206             type: "value",
207             expr: expr,
208             element: e
209           });
210         }
211       }
212       var content = e.getAttribute("data-content");
213       if(content){
214         var expr = content.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/);
215         var name = expr[1];
216         expr = content;
217         var value = evaluateExpression(expr);
218         setContent(e,value);
219         mapping[name] = mapping[name] || [];
220         mapping[name].push({
221           what: "content",
222           type: "value",
223           expr: expr,
224           element: e
225         });
226       }
227       var getter = e.getAttribute("data-getter");
228       if(getter){
229         getter = getter.split("¦");
230         for(var i=0;i<getter.length;i++){
231           var g = getter[i].split(":");
232           var expr = g[1];
233           var name = expr.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/)[1];
234           var attr = g[0];
235           mapping[name] = mapping[name] || [];
236           mapping[name].push({
237             which: attr,
238             type: "getter",
239             expr: expr,
240             element: e
241           });
242         }
243       }
244       var setter = e.getAttribute("data-setter");
245       if(setter){
246         setter = setter.split("¦");
247         for(var i=0;i<setter.length;i++){
248           var s = setter[i].split(":");
249           var expr = s[1];
250           var name = expr.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/)[1];
251           var attr = s[0];
252           mapping[name] = mapping[name] || [];
253           mapping[name].push({
254             which: attr,
255             type: "setter",
256             expr: expr,
257             element: e
258           });
259         }
260       }
261       var updateScope = function(attr){
262         var value = e[attr] || e.getAttribute(attr);
263         for(var name in mapping){
264           var maps = mapping[name];
265           for(var i in maps){
266             var map = maps[i];
267             if(map.type!="value"||map.which!=attr)
268               continue;
269             (new Function("s","v","if(s."+map.expr+".toString()!=v)s."+map.expr+"=Object(s."+map.expr+").constructor(v);")).call(scope,scope,value);
270           }
271         }
272       };
273       if("MutationObserver" in window){
274         var observer = new MutationObserver(function(mutations){
275           mutations.forEach(function(mutation){;
276             updateScope(mutation.attributeName);
277           });
278         });
279         observer.observe( e, /** @type {!MutationObserverInit} */ ({ attributes: true }) );
280       }else if("value" in e){
281         e.addEventListener("change",updateScope.bind(null,"value"),false);
282         e.addEventListener("input",updateScope.bind(null,"value"),false);
283       }
284       if("value" in e)
285         e.addEventListener("change",function(){
286           e.setAttribute("value",e.value);
287         });
288
289       var map = e.getAttribute("data-map");
290       if(map){
291         var v = map.split(":");
292         var expr = v[0].match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)(.*)/);
293         var name = expr[1];
294         expr=expr[2];
295         var setup_mapping = function(){
296           if(!(v[1] in templates)){
297             ( requiredTemplates[v[1]] = requiredTemplates[v[1]] || [] ).push( setup_mapping );
298             return;
299           }
300           var target = scope[name];
301           if(target)
302             target = Object((new Function("v","return v"+expr+";"))(target));
303           if(!target)
304             target=[];
305 //          clearContent(e);
306           if(!("length" in target))
307             target=[target];
308           e._content = e._content || {
309             subScopes: [],
310             subScopeInfos: []
311           };
312           Object.observe(target,function(changes){
313             syncLists(e._content,target,e,templates[v[1]]);
314           });
315           syncLists(e._content,target,e,templates[v[1]]);
316         };
317         ( mapping[v[0]] = mapping[v[0]] || [] ).push({
318           "type": "map",
319           "update": setup_mapping
320         });
321         setup_mapping();
322       }
323
324       if(!map)
325         for(var i=0;i<e.children.length;i++)
326           setup(e.children[i]);
327     }
328
329     function syncLists(contentDatas,orig,e,t){
330       var b = [];
331       for(var i=0;i<orig.length;i++){
332         if(!(orig[i] instanceof Object))
333           continue;
334         if(b.indexOf(orig[i])!=-1)
335           continue;
336         b.push(orig[i]);
337       }
338       var a = contentDatas.subScopes;
339       var d = contentDatas.subScopeInfos;
340       for(var i=a.length;i--;){ // remove elements / objects
341         if(b.indexOf(a[i])!=-1)
342           continue;
343         if(d[i].element.parentNode)
344           d[i].element.parentNode.removeChild(d[i].element);
345         a.splice(i,1);
346         d.splice(i,1);
347       }
348       for(var i=0;i<b.length;i++){ // add elements / objects
349         var j = a.indexOf(b[i]);
350         if(j!=-1){
351           d[j].index = i;
352         }else{
353           var newInfo = {
354             element: t.instance(b[i]),
355             index: i
356           };
357           if(!d.length){
358             e.appendChild(newInfo.element);
359           }else{
360             var last = d[d.length-1].element;
361             if(last.parentNode==e){
362               e.insertBefore(newInfo.element,last.nextSibling);
363             }else{
364               e.appendChild(newInfo.element);
365             }
366           }
367           a.push(b[i]);
368           d.push(newInfo);
369         }
370       }
371       for(var i=0;i<d.length;i++){ // move elements / objects to desired index
372         var x = d[i];
373         if(x.index==i)
374           continue;
375         var ae = x.element;
376         var be = d[x.index].element;
377         arraySwapValues(d,i,x.index);
378         arraySwapValues(a,i,x.index);
379         var ap = ae.parentNode;
380         var bp = be.parentNode;
381         if(ap&&bp){
382           var an = ae.nextSibling;
383           var bn = be.nextSibling;
384           bp.insertBefore(ae,bn);
385           ap.insertBefore(be,an);
386         }
387       }
388     }
389
390     setup(this.root);
391
392     var observerFunc = function(changes){
393       changes.forEach(function(ch){
394         update(ch.name,ch.type);
395       });
396     }
397     var observerObj = Object.observe(scope,observerFunc);
398     sc.observer = {
399       obj: observerObj,
400       func: observerFunc
401     };
402
403     for(var i in scope){
404       update(i,"add");
405     }
406
407   };
408   this.instance = function(scope){
409     var instance = new Instance(scope);
410     return instance.root;
411   };
412 }
413
414 function arraySwapValues(a,i,j){
415   a[i]=[a[j],a[j]=a[i]][0];
416 }
417
418 function compileTemplate(e){
419   var name = e.getAttribute("data-template");
420   if(!name&&e instanceof HTMLHtmlElement)
421     name = "root";
422   if(e.parentNode&&!(e instanceof HTMLHtmlElement))
423     e.parentNode.removeChild(e);
424   e.templateName = name;
425   var t = templates[name] = new Template(e);
426   if(name in requiredTemplates){
427     while(requiredTemplates[name].length){
428       requiredTemplates[name].pop()();
429     }
430     delete requiredTemplates[name];
431   }
432   return t;
433 }
434
435 function compileTemplates( templates ){
436   if( "querySelectorAll" in templates )
437     templates = templates.querySelectorAll("[data-template]");
438   for(var i=0;i<templates.length;i++){
439     compileTemplate(templates[i]);
440   }
441 }
442
443 var base = "/";
444
445 function loadTemplate(name){
446   var url = base+"templates/"+name+".html";
447   var xhr = new XMLHttpRequest();
448   xhr.open("GET",url,true);
449   xhr.onload = function(e){
450     var result = null;//this.responseXML;
451     if(!result){
452       result = document.createDocumentFragment();
453       var div = document.createElement("div");
454       div.innerHTML = this.responseText;
455       result.appendChild(div);
456     }
457     compileTemplates(result);
458   };
459   xhr.send();
460 }
461
462 var initialized = false;
463
464 function initMVSync(ignoreReadystate){
465   if(initialized)
466     return;
467   if(!( document.readyState == "complete" 
468      || document.readyState == "loaded" 
469      || document.readyState == "interactive"
470      || ignoreReadystate
471   )) return;
472   if(!("observe" in Object))
473     return;
474   initialized = true;
475   if(window['templateRoot']){
476     base = window['templateRoot'] + "/";
477   }else if(document.querySelector("[data-template-root]")){
478     base = document.querySelector("[data-template-root]").getAttribute("data-template-root") + "/";
479   }
480   init();
481   compileTemplates(document);
482   var t = compileTemplate(document.documentElement);
483   t.instance(model);
484 };
485
486 window['initMVSync'] = initMVSync;
487
488 })();
489
490 addEventListener("load",initMVSync.bind(null,true));
491 addEventListener("DOMContentLoaded",initMVSync.bind(null,true));
492 initMVSync(false);
493