patch = function(tgtParent, srcParent, tgtBefore, tgtAfter, preservations, results) { var copyFunc = function(t, s) { LiveRange.transplantTag(TAG, t, s); }; var patcher = new Patcher( tgtParent, srcParent, tgtBefore, tgtAfter); var visitNodes = function(parent, before, after, func) { for(var n = before ? before.nextSibling : parent.firstChild; n && n !== after; n = n.nextSibling) { if (func(n) !== false && n.firstChild) visitNodes(n, null, null, func); } }; // results arg is optional; it is mutated if provided; returned either way results = (results || {}); // array of LiveRanges that were successfully preserved from // the region preservations var regionPreservations = (results.regionPreservations = results.regionPreservations || []); var lastTgtMatch = null; visitNodes(srcParent, null, null, function(src) { // XXX inefficient to scan for match for every node! // We could at least skip non-element nodes, except for "range matches" // used for constant chunks, which may begin on a non-element. // But really this shouldn't be a linear search. var pres = _.find(preservations, function (p) { // find preserved region starting at `src`, if any return p.type === 'region' && p.newRange.firstNode() === src; }) || _.find(preservations, function (p) { // else, find preservation of `src` return p.type === 'node' && p.to === src; }); if (pres) { var tgt = (pres.type === 'region' ? pres.fromStart : pres.from); if (! lastTgtMatch || DomUtils.compareElementIndex(lastTgtMatch, tgt) < 0) { if (pres.type === 'region') { // preserved region for constant landmark if (patcher.match(pres.fromStart, pres.newRange.firstNode(), copyFunc, true)) { patcher.skipToSiblings(pres.fromEnd, pres.newRange.lastNode()); // without knowing or caring what DOM nodes are in pres.newRange, // transplant the range data to pres.fromStart and pres.fromEnd // (including references to enclosing ranges). LiveRange.transplantRange( pres.fromStart, pres.fromEnd, pres.newRange); regionPreservations.push(pres.newRange); } } else if (pres.type === 'node') { if (patcher.match(tgt, src, copyFunc)) { // match succeeded lastTgtMatch = tgt; if (tgt.firstChild || src.firstChild) { if (tgt.nodeName !== "TEXTAREA" && tgt.nodeName !== "SELECT") { // Don't patch contents of TEXTAREA tag (which are only the // initial contents but may affect the tag's .value in IE) or of // SELECT (which is specially handled in _copyAttributes). // Otherwise recurse! patch(tgt, src, null, null, preservations); } } return false; // tell visitNodes not to recurse } } } } return true; }); patcher.finish(); return results; }; // A Patcher manages the controlled replacement of a region of the DOM. // The target region is changed in place to match the source region. // // The target region consists of the children of tgtParent, extending from // the child after tgtBefore to the child before tgtAfter. A null // or absent tgtBefore or tgtAfter represents the beginning or end // of tgtParent's children. The source region consists of all children // of srcParent, which may be a DocumentFragment. // // To use a new Patcher, call `match` zero or more times followed by // `finish`. // // A match is a correspondence between an old node in the target region // and a new node in the source region that will replace it. Based on // this correspondence, the target node is preserved and the attributes // and children of the source node are copied over it. The `match` // method declares such a correspondence. A Patcher that makes no matches, // for example, just removes the target nodes and inserts the source nodes // in their place. // // Constructor: Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) { this.tgtParent = tgtParent; this.srcParent = srcParent; this.tgtBefore = tgtBefore; this.tgtAfter = tgtAfter; this.lastKeptTgtNode = null; this.lastKeptSrcNode = null; }; // Advances the patching process up to tgtNode in the target tree, // and srcNode in the source tree. tgtNode will be preserved, with // the attributes of srcNode copied over it, in essence identifying // the two nodes with each other. The same treatment is given to // any parents of the nodes that are newly implicated as corresponding. // In the process of traversing from the last matched nodes to these // ones, all nodes "in between" in the target document, at any level, // are removed, and all nodes "in between" in the source document // are copied over to their appropriate positions. // // For example, if match() is called only once, and then finish() // is called, the effect is to preserve tgtNode, its children, // and its ancestors (parent chain), while swapping out all its // siblings and the siblings of its ancestors, so that the target // tree is mutated to look like the source tree did. // // The caller is responsible for ensuring the precondition that // subsequent tgtNodes and subsequent srcNodes are strictly "in order." // The ordering referred to here is a partial order in which A comes // before B if their tags would be disjoint in HTML, i.e. the end of // A comes before the beginning of B. Put another way, there is some // ancestor of A and some ancestor of B that have the same parent, // are different, and are in order. // // There are other requirements for two nodes to be "matched," // but match() can detect them and exit gracefully returning false. // For example, the tag-names must be the same, and the tag-names // of their parents. More subtly, it may be impossible to match // the parents of tgtNode or srcNode because they have been // previously matched. If we are to match a series of P tags // that are each inside one DIV, for example, is it the same DIV // or not? If the source and target disagree, we will have to // reparent one of the Ps. Users should not be moving identified // nodes, but we want to still be correct (fall back on replacement) // if they do. // // If false is returned, the match was impossible, but patching // can continue and will still be otherwise correct. The next call // to match() must still obey the order constraint, as the patcher // internally only moves forwards and patches as it goes. // // copyCallback is called on every new matched (tgt, src) pair // right after copying attributes. It's a good time to transplant // liveranges and patch children. Patcher.prototype.match = function( tgtNode, srcNode, copyCallback, onlyAdvance) { // last nodes "kept" (matched/identified with each other) var lastKeptTgt = this.lastKeptTgtNode; var lastKeptSrc = this.lastKeptSrcNode; // nodes to match and keep, this time around var tgt = tgtNode; var src = srcNode; if ((! tgt) != (! src)) { return false; // truthinesses don't match } var starting = ! lastKeptTgt; var finishing = ! tgt; if (! starting) { // move lastKeptTgt/lastKeptSrc forward and out, // until they are siblings of tgt/src or of an ancestor of tgt/src, // replacing as we go. If tgt/src is falsy, we make it to the // top level. while (lastKeptTgt.parentNode !== this.tgtParent && ! (tgt && DomUtils.elementContains(lastKeptTgt.parentNode, tgt))) { // Last-kept nodes are inside parents that are not // parents of the newly matched nodes. Must finish // replacing their contents and back out. this._replaceNodes(lastKeptTgt, null, lastKeptSrc, null); lastKeptTgt = lastKeptTgt.parentNode; lastKeptSrc = lastKeptSrc.parentNode; } // update instance vars; there's no going back inside these nodes this.lastKeptTgtNode = lastKeptTgt; this.lastKeptSrcNode = lastKeptSrc; // Make sure same number of levels of "moving up" are // appropriate for src as well, i.e. we aren't trying // to match in (, ) // after matching , or vice versa. In other words, // if tag names and depths match, but identities of parents // are inconsistent relative to previous matches, we catch it // here. In the example, lastKeptTgt would be the node // on the left, which is not sibling of or of an ancestor // of on the right. If the example were reversed, // lastKeptTgt would be the first node, which is an // ancestor of on the left rather than a sibling of an // ancestor. if (! finishing && (DomUtils.elementContains(lastKeptSrc, src) || ! (lastKeptSrc.parentNode === this.srcParent || DomUtils.elementContains(lastKeptSrc.parentNode, src)))) { return false; } } if (finishing) { this._replaceNodes(lastKeptTgt, null, lastKeptSrc, null, this.tgtParent, this.srcParent); } else { // Compare tag names and depths to make sure we can match nodes... if (! onlyAdvance) { if (tgt.nodeName !== src.nodeName) return false; } // Look at tags of parents until we hit parent of last-kept, // which we know is ok. for(var a=tgt.parentNode, b=src.parentNode; a !== (starting ? this.tgtParent : lastKeptTgt.parentNode); a = a.parentNode, b = b.parentNode) { if (b === (starting ? this.srcParent : lastKeptSrc.parentNode)) return false; // src is shallower, b hit top first if (a.nodeName !== b.nodeName) return false; // tag names don't match } if (b !== (starting ? this.srcParent : lastKeptSrc.parentNode)) { return false; // src is deeper, b didn't hit top when a did } var firstIter = true; // move tgt and src backwards and out, replacing as we go while (true) { if (! (firstIter && onlyAdvance)) { if (tgt.nodeType === 1) /* ELEMENT */ Patcher._copyAttributes(tgt, src); if (copyCallback) copyCallback(tgt, src); } firstIter = false; if ((starting ? this.tgtParent : lastKeptTgt.parentNode) === tgt.parentNode) { // we've worked our way up to the same level as the last-kept nodes this._replaceNodes(lastKeptTgt, tgt, lastKeptSrc, src); break; } else { this._replaceNodes(null, tgt, null, src); // move up to keep (match) parents as well tgt = tgt.parentNode; src = src.parentNode; } } } this.lastKeptTgtNode = tgtNode; this.lastKeptSrcNode = srcNode; return true; }; // After a match, skip ahead to later siblings of the last kept nodes, // without performing any replacements. Patcher.prototype.skipToSiblings = function(tgt, src) { var lastTgt = this.lastKeptTgtNode; var lastSrc = this.lastKeptSrcNode; if (! (lastTgt && lastTgt.parentNode === tgt.parentNode)) return false; if (! (lastSrc && lastSrc.parentNode === src.parentNode)) return false; this.lastKeptTgtNode = tgt; this.lastKeptSrcNode = src; return true; }; // Completes patching assuming no more matches. // // Patchers are single-use, so no more methods can be called // on the Patcher. Patcher.prototype.finish = function() { return this.match(null, null); }; // Replaces the siblings between tgtBefore and tgtAfter (exclusive on both // sides) with the siblings between srcBefore and srcAfter (exclusive on both // sides). Falsy values indicate start or end of siblings as appropriate. // // Precondition: tgtBefore and tgtAfter have same parent; either may be falsy, // but not both, unless optTgtParent is provided. Same with srcBefore/srcAfter. Patcher.prototype._replaceNodes = function( tgtBefore, tgtAfter, srcBefore, srcAfter, optTgtParent, optSrcParent) { var tgtParent = optTgtParent || (tgtBefore || tgtAfter).parentNode; var srcParent = optSrcParent || (srcBefore || srcAfter).parentNode; // deal with case where top level is a range if (tgtParent === this.tgtParent) { tgtBefore = tgtBefore || this.tgtBefore; tgtAfter = tgtAfter || this.tgtAfter; } if (srcParent === this.srcParent) { srcBefore = srcBefore || this.srcBefore; srcAfter = srcAfter || this.srcAfter; } // remove old children var n; while ((n = tgtBefore ? tgtBefore.nextSibling : tgtParent.firstChild) && n !== tgtAfter) { tgtParent.removeChild(n); } // add new children var m; while ((m = srcBefore ? srcBefore.nextSibling : srcParent.firstChild) && m !== srcAfter) { tgtParent.insertBefore(m, tgtAfter || null); } }; // Copy HTML attributes of node `src` onto node `tgt`. // // The effect we are trying to achieve is best expresed in terms of // HTML. Whatever HTML generated `tgt`, we want to mutate the DOM element // so that it is as if it were the HTML that generated `src`. // We want to preserve JavaScript properties in general (tgt.foo), // while syncing the HTML attributes (tgt.getAttribute("foo")). // // This is complicated by form controls and the fact that old IE // can't keep the difference straight between properties and attributes. Patcher._copyAttributes = function(tgt, src) { var srcAttrs = src.attributes; var tgtAttrs = tgt.attributes; // Determine whether tgt has focus; works in all browsers // as of FF3, Safari4 var targetFocused = (tgt === document.activeElement); ///// Clear current attributes if (tgt.style.cssText) tgt.style.cssText = ''; var isRadio = false; var finalChecked = null; if (tgt.nodeName === "INPUT") { // Record for later whether this is a radio button. isRadio = (tgt.type === 'radio'); // Figure out whether this should be checked or not. If the re-rendering // changed its idea of checkedness, go with that; otherwsie go with whatever // the control's current setting is. if (isRadio || tgt.type === 'checkbox') { var tgtOriginalChecked = !!tgt._sparkOriginalRenderedChecked && tgt._sparkOriginalRenderedChecked[0]; var srcOriginalChecked = !!src._sparkOriginalRenderedChecked && src._sparkOriginalRenderedChecked[0]; // For radio buttons, we previously saved the checkedness in an expando // property before doing some DOM operations that could wipe it out. For // checkboxes, we can just use the checked property directly. var tgtCurrentChecked = tgt._currentChecked ? tgt._currentChecked[0] : tgt.checked; if (tgtOriginalChecked === srcOriginalChecked) { finalChecked = tgtCurrentChecked; } else { finalChecked = srcOriginalChecked; tgt._sparkOriginalRenderedChecked = [finalChecked]; } } } for(var i=tgtAttrs.length-1; i>=0; i--) { var attr = tgtAttrs[i]; // In old IE, attributes that are possible on a node // but not actually present will show up in this loop // with specified=false. All other browsers support // 'specified' (because it's part of the spec) and // set it to true. if (! attr.specified) continue; var name = attr.name; // Filter out attributes that are indexable by number // but not by name. This kills the weird "propdescname" // attribute in IE 8. if (! tgtAttrs[name]) continue; // Some properties don't mutate well, and we simply // don't try to patch them. For example, you can't // change a control's type in IE. if (name === "id" || name === "type") continue; // Removing a radio button's "name" property and restoring // it is harmless in most browsers but breaks in IE 7. // It seems unlikely enough that a radio button will // sometimes have a group and sometimes not. if (isRadio && name === "name") continue; // Never delete the "value" attribute: we have special three-way diff logic // for it at the end. if (name === "value") continue; // Removing 'src' (e.g. in an iframe) can only be bad. if (name === "src") continue; // We want to patch any HTML attributes that were specified in the // source, but preserve DOM properties set programmatically. // Old IE makes this difficult by exposing properties as attributes. // Expando properties will even appear in innerHTML, though not if the // value is an object rather than a primitive. // // We use a heuristic to determine if we are looking at a programmatic // property (an expando) rather than a DOM attribute. // // Losing jQuery's expando (whose value is a number) is very bad, // because it points to event handlers that only jQuery can detach, // and only if the expando is in place. var possibleExpando = tgt[name]; if (possibleExpando && (typeof possibleExpando === "object" || /^jQuery/.test(name))) continue; // for object properties that surface attributes only in IE tgt.removeAttributeNode(attr); } ///// Copy over src's attributes if (tgt.mergeAttributes) { // IE code path: // // Only IE (all versions) has mergeAttributes. // It's probably a good bit faster in old IE than // iterating over all the attributes, and the treatment // of form controls is sufficiently different in IE from // other browsers that we keep the special cases separate. // Don't copy _sparkOriginalRenderedValue, though. var srcExpando = src._sparkOriginalRenderedValue; src.removeAttribute('_sparkOriginalRenderedValue'); tgt.mergeAttributes(src); if (srcExpando) src._sparkOriginalRenderedValue = srcExpando; if (src.name) tgt.name = src.name; } else { // Non-IE code path: for(var i=0, L=srcAttrs.length; i's value if possible (ie, ignore // any