UNPKG

sumeru

Version:

A Realtime Javascript RIA Framework For Mobile WebApp

489 lines (416 loc) 14.4 kB
/** * usage: fw.domdiff.convert(htmlfragment,target); */ (function(fw){ fw.addSubPackage('domdiff'); var util = { make : function(t,c) { var d = document.createElement(t); if(c){ if(c.innerHTML){ d.innerHTML = c.innerHTML; }else{ d.innerHTML = c; } }; return d; }, arrayCopy : function(arr) { var narr = [], a, l=arr.length; for(a=0; a<l; a++) { narr[a] = arr[a]; } return narr; }, mergeAttrs : function(e1,e2){ var attrs = [], e1as = e1.attributes, e2as = e2.attributes, len; if(e1as && e2as) { var helper = document.createElement(e1.nodeName); for(len = e1as.length-1;len>=0;len--){ helper.setAttribute(e1as[len].nodeName,e1as[len].nodeValue); } for(len = e2as.length-1;len>=0;len--){ helper.setAttribute(e2as[len].nodeName,e2as[len].nodeValue); } for(len = helper.attributes.length-1;len>=0;len--){ attrs.push(helper.attributes[len].nodeName); } }; return attrs; }, snapshot : function(list) { var newlist = [], i=0, last=list.length; for(;i<last;i++) { newlist.push(list[i]); } return newlist; }, hashCode : function(str,undef) { if(str===null || str===undef) return 0; var hash = 0, i, last = str.length; for (i = 0; i < last; ++i) { hash = (hash * 31 + str.charCodeAt(i)) & 0xFFFFFFFF; } return hash; }, /*递归实现element的hash*/ hashAll : function(element, undef) { var child, last = (element.childNodes === undef ? 0 : element.childNodes.length), hash = 0, hashString = element.nodeName; if(element.attributes) { var attr, a, attributes = element.attributes, len = attributes.length; for (a=0; a<len; a++) { attr = attributes[a]; hashString += attr.nodeName+":"+attr.nodeValue; } } hash = util.hashCode( (hashString+element.textContent).replace(/\s+/g,'')); for(child = last-1; child >=0; child--) { hash = (hash * 31 + util.hashAll(element.childNodes[child])) & 0xFFFFFFFF; } element["hashCode"] = hash; return hash; }, find: function(e,treeRoute) { var route = util.snapshot(treeRoute), pos = route.splice(0,1)[0]; while(pos!==-1) { e = e.childNodes[pos]; pos = route.splice(0,1)[0]; } return e; }, delcomment: function(e){ if(e.nodeType==8){ e.parentNode.removeChild(e); }else{ var len = e.childNodes.length-1; while(len>=0){ util.delcomment(e.childNodes[len]); len--; } } } } var getDiff = function(e1, e2) { var route = equal(e1,e2), routes = [route], newRoute; while(typeof route === "object") { newRoute = equal(e1,e2,util.arrayCopy(route)); routes.push(newRoute); route = newRoute; } // 删除最后一项,因为最后一项代表的是wrap的attributes对比结果。不需要care wrap的对比结果 if(routes.length>1) { routes.splice(routes.indexOf(0), 1); } return routes; }; var equal = function(e1, e2, after) { var soffset = (after && after.length!==0 ? after.splice(0,1)[0] : 0); if(soffset === -1) { return 0; } if(e1.nodeType !== e2.nodeType) { return -1; } // shortcut handling for text? if(e1.nodeType===3 && e2.nodeType===3) { if(e1.textContent.trim() != e2.textContent.trim()) { return -1; } return 0; } // different element (2)? if(e1.nodeName !== e2.nodeName) { return -1; } // different content? if(e1.childNodes.length !== e2.childNodes.length) { return -1; } // Different child node list? // Find where the first difference is var i, last = e1.childNodes.length, eq, ret; for(i=soffset; i<last; i++) { // recurse to see if these children differ eq = equal(e1.childNodes[i], e2.childNodes[i], after); if(eq !== 0) { // (first) difference found. "eq" will indicate // which childNodes position the diff is found at. return [i].concat(eq); } } // different attributes? var attrs = util.mergeAttrs(e1,e2), a, last = attrs.length, attr, a1, a2; for(a=0; a<last; a++) { attr = attrs[a]; a1 = e1.getAttribute(attr); a2 = e2.getAttribute(attr); if(a1==a2 || (!a1 && a2=="") || (!a2 && a1=="")) continue; return -1; } // nothing left to fail on - consider // these two elements equal. return 0; }; /** * Do these elements agree on their HTML attributes? * * @return array of [differing attribute, value in e1, value in e2] triplets */ function outerEquality(e1, e2) { var diff = []; // do the tags agree? if(e1.nodeType===1 && e2.nodeType===1) { if(e1.nodeName !== e2.nodeName) { diff.push(["nodeName",e1.nodeName,e2.nodeName]); } } // do the attributes agree? if(e1.attributes && e2.attributes) { var attributes = e1.attributes, len = attributes.length, a, a1, a2, attr; // attribute insertion/modification diff for (a=0; a<len; a++) { attr = attributes[a].nodeName; a1 = e1.getAttribute(attr); a2 = e2.getAttribute(attr); if(a1==a2) continue; diff.push([attr,a1,a2]); } // attribute removal diff attributes = e2.attributes; len = attributes.length; for (a=0; a<len; a++) { attr = attributes[a].nodeName; a1 = e1.getAttribute(attr); a2 = e2.getAttribute(attr); if(a1==a2) continue; diff.push([attr,a1,a2]); } } return diff; }; /** * Do these elements agree on their content, * based on the .childNodes NodeSet? * * @return a diff tree between the two elements */ var innerEquality = function(e1, e2) { util.hashAll(e1); util.hashAll(e2); c1 = util.snapshot(e1.childNodes); c2 = util.snapshot(e2.childNodes); var localdiff = childDiff(c1,c2); return (localdiff.insertions.length > 0 || localdiff.removals.length > 0 || localdiff.relocations.length > 0 ? localdiff : false); }; /** * Does a nodeset snapshot of an element's * .childNodes contain an element that has * <hash> as hashing number? * * @return -1 if not contained, or the * position in the snapshot if * it is contained. */ function getPositions(list, reference) { var hash = reference.hashCode, c, last = list.length, child, result = []; for(c=0; c<last; c++) { child = list[c]; if(child.hashCode === hash) { result.push(c); } } return result; } /** * Create a diff between .childNode * snapshots c1 and c2. * * @return a local content diff */ function childDiff(c1, c2) { var relocations = [], insertions = [], removals = []; var c, last=c1.length, child, hash, positions, pos; for(c=0; c<last; c++) { child = c1[c]; positions = getPositions(c2, child); if(positions.length===0) continue; if(positions.length>1) continue; pos = positions[0]; if(c!==pos && getPositions(c1, child).length <= 1) { relocations.push(c2[pos]); child["marked"] = true; c2[pos]["marked"] = true; } else if(c===pos) { child["marked"] = true; c2[pos]["marked"] = true; } } last = c2.length; for(c=0; c<last; c++) { child = c2[c]; if(!child["marked"]) { removals.push(child); } } last = c1.length; for(c=0; c<last; c++) { child = c1[c]; if(!child["marked"]) { insertions.push(child); } } var localdiff = { c1: util.snapshot(c1), c2: util.snapshot(c2), relocations: relocations, insertions: insertions, removals: removals }; return localdiff; } /** * newhtml : new dom * targetElement : 需要修改的dom */ var convert = function(newhtml,targetElement) { var d1 = util.make(targetElement.nodeName,newhtml),d2 = targetElement; //删除需要diff的dom中的注释。 util.delcomment(d1); util.delcomment(d2); var routes = getDiff(d1,d2), route, iroute, d, lastRoute = routes.length, v; if(lastRoute===1 && routes[0]===0) { return false; } if(lastRoute>fw.config.get('domdiffnum')){ targetElement.innerHTML = newhtml; return false; } for(d = 0; d < lastRoute; d++) { if (routes[d] === 0) { continue; } // rewrite so we do can resolve the top-level diff if (routes[d] === -1) { routes[d] = [-1]; } // follow the route to the elements route = util.arrayCopy(routes[d]), iroute = util.arrayCopy(routes[d]); var e1 = d1, e2 = d2, e = route.splice(0,1)[0]; while (e !== -1) { e1 = e1.childNodes[e]; e2 = e2.childNodes[e]; e = route.splice(0,1)[0]; } if(e1.nodeType===3 && e2.nodeType===3) { var parent = e2.parentNode; parent.replaceChild(e1.cloneNode(),e2); } else { var complexDiff = innerEquality(e1,e2), pos, last, entry; //不对比最外层的attributes if(routes[d].length > 1){ var outerDiff = outerEquality(e1,e2); }else{ var outerDiff = []; } if(outerDiff.length>0) { last = outerDiff.length; for(pos=0; pos<last; pos++) { entry = outerDiff[pos]; if(entry[0]==="nodeName") { var element = util.find(d2,iroute), newElement = document.createElement(entry[1]); while(element.childNodes.length>0) { newElement.appendChild(element.childNodes[0]); } for(var alen=element.attributes.length-1;alen>=0;alen--){ newElement.setAttribute(element.attributes[alen].nodeName,element.attributes[alen].nodeValue); } element.parentNode.replaceChild(newElement, element); }else { var element = util.find(d2,iroute); if(entry[1]==null) { element.removeAttribute(entry[0]); }else { element.setAttribute(entry[0], entry[1]); } } } } if(!complexDiff) { continue; } var parent = util.find(d2,iroute); var newParent = util.find(d1,iroute); //处理remove的节点 last = complexDiff.removals.length; if(last>0) { for(pos=last-1; pos>=0; pos--) { entry = complexDiff.removals[pos]; parent.removeChild(entry); } } //处理insert的节点,这时是无序插入的。 last = complexDiff.insertions.length; if(last>0) { for(pos=0; pos<last; pos++) { entry = complexDiff.insertions[pos]; var newEntry = entry.cloneNode(true); newEntry["hashCode"] = entry.hashCode; parent.appendChild(newEntry);//克隆的目的是为了包装参照树不被改变。 } } /** var test = function(e,id){ var last = e.length; var arr = []; for(var i=0;i<last;i++){ arr.push(e[i][id]); } }*/ //重新排序 var newNodes = newParent.childNodes, nodes = parent.childNodes, oldPos,newPos; /** test(newNodes,'hashCode'); test(nodes,'hashCode'); */ last = newNodes.length; for(newPos=0; newPos<last; newPos++) { oldPos = getPositions(nodes,newNodes[newPos]); if(oldPos.length<1){ console.error('error: convert error'); continue; } if(oldPos.length>1){ if(oldPos[0]==newPos) continue; oldPos = oldPos[oldPos.length-1]; entry = nodes[oldPos]; }else{ oldPos = oldPos[0]; entry = nodes[oldPos]; } if(oldPos===newPos) continue; parent.insertBefore(entry,nodes[newPos]); } } } return true; }; fw.domdiff.__reg('convert', convert, 'private'); })(sumeru);