diff --git a/packages/spark/spark.js b/packages/spark/spark.js index f99425ec4b..e8e9358677 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -263,6 +263,121 @@ Spark.render = function (htmlFunc) { return frag; }; + +// Find all of all nodes and regions that should be preserved in +// patching. Return a list of objects. There are two kinds of objects +// in the list: +// +// A preserved node: +// {type: "node", from: Node, to: Node} +// +// A preserved (constant) region: +// {type: "region", fromStart: Node, fromEnd: Node, +// newRange: LiveRange} +// +// `existingRange` is the range in the document whose contents are to +// be replaced. `newRange` holds the new contents and is not part of +// the document DOM tree. The implementation will temporarily reparent +// the nodes in `newRange` into the document to check for selector matches. +var PreservationController = function () { + this.roots = []; // keys 'landmark', 'fromRange', 'toRange' + this.regionPreservations = []; +}; + +_.extend(PreservationController.prototype, { + addRoot: function (context, preserve, fromRange, toRange) { + var self = this; + self.roots.push({ context: context, preserve: preserve, + fromRange: fromRange, toRange: toRange}); + }, + addAllRoots: function (existingRange, newRange) { + var self = this; + // Consider landmarks that got rerendered and were matched + visitMatchingLandmarks(existingRange, newRange, function (from, to) { + self.addRoot(from.containerNode(), to.preserve, from, to); + }); + + // Consider landmarks enclosing the updated region + var walk = existingRange; + while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) + self.addRoot(walk.containerNode(), walk.preserve, + existingRange, newRange); + }, + addConstantRegions: function (existingRange, newRange) { + var self = this; + visitMatchingLandmarks(existingRange, newRange, function (from, to) { + if (to.constant) + self.regionPreservations.push({ + type: "region", + fromStart: from.firstNode(), fromEnd: from.lastNode(), + newRange: to + }); + }); + }, + computePreservations: function (existingRange, newRange) { + var self = this; + var preservations = _.clone(self.regionPreservations); + + var visitLabeledNodes = function (context, clipRange, nodeLabeler, selector, func) { + var nodes = DomUtils.findAllClipped( + context, selector, clipRange.firstNode(), clipRange.lastNode()); + + _.each(nodes, function (n) { + var label = nodeLabeler(n); + label && func(n, label); + }); + }; + + // Find the old incarnation of each of the preserved nodes + _.each(self.roots, function (root) { + root.fromNodesByLabel = {}; + _.each(root.preserve, function (nodeLabeler, selector) { + root.fromNodesByLabel[selector] = {}; + visitLabeledNodes(root.context, root.fromRange, nodeLabeler, selector, function (n, label) { + root.fromNodesByLabel[selector][label] = n; + }); + }); + }); + + // Temporarily put newRange into the document so that we can do + // properly contextualized selector queries against it. + // + // Create a temporary range around newRange, and also around any enclosing + // ranges that happen to also start and end on those nodes. It is ok + // to temporarily put these in the document as well, because CSS selectors + // don't care and we will put them back. `tempRange` will hold our place + // in the tree `newRange` came from. + var tempRange = new LiveRange(Spark._TAG, newRange.firstNode(), newRange.lastNode()); + var commentFrag = document.createDocumentFragment(); + commentFrag.appendChild(document.createComment("")); + var newRangeFrag = tempRange.replace_contents(commentFrag); + // `wrapperRange` will mark where we inserted newRange into the document. + var wrapperRange = new LiveRange(Spark._TAG, newRangeFrag); + existingRange.insert_before(newRangeFrag); + + _.each(self.roots, function (root) { + _.each(root.preserve, function (nodeLabeler, selector) { + visitLabeledNodes(root.context, root.toRange, nodeLabeler, selector, function (n, label) { + var match = root.fromNodesByLabel[selector][label]; + if (match) { + preservations.push({ type: "node", from: match, to: n }); + root.fromNodesByLabel[selector][label] = null; + } + }); + }); + }); + + // Extraction is legal because we're just taking the document + // back to the state it was in before insertBefore. + var extractedFrag = wrapperRange.extract(); + wrapperRange.destroy(); + tempRange.replace_contents(extractedFrag); + tempRange.destroy(); + + return preservations; + } +}); + // Modify `range` so that it matches the result of // Spark.render(htmlFunc). `range` must be in `document` (that is, // onscreen.) If the old contents had any landmarks that match @@ -295,7 +410,11 @@ Spark.renderToRange = function (range, htmlFunc) { }); // compute preservations - var preservations = computePreservations(range, tempRange); + var pc = new PreservationController; + pc.addAllRoots(range, tempRange); + pc.addConstantRegions(range, tempRange); + var preservations = pc.computePreservations(range, tempRange); + tempRange.destroy(); // patch (using preservations) @@ -732,107 +851,6 @@ var visitMatchingLandmarks = function (range1, range2, func) { }); }; -// Find all of all nodes and regions that should be preserved in -// patching. Return a list of objects. There are two kinds of objects -// in the list: -// -// A preserved node: -// {type: "node", from: Node, to: Node} -// -// A preserved (constant) region: -// {type: "region", fromStart: Node, fromEnd: Node, -// newRange: LiveRange} -// -// `existingRange` is the range in the document whose contents are to -// be replaced. `newRange` holds the new contents and is not part of -// the document DOM tree. The implementation will temporarily reparent -// the nodes in `newRange` into the document to check for selector matches. -var computePreservations = function (existingRange, newRange) { - var preservations = []; - - var nodePreserveRoots = []; // keys 'landmark', 'fromRange', 'toRange' - - // Consider landmarks that got rerendered and were matched - visitMatchingLandmarks(existingRange, newRange, function (from, to) { - if (to.constant) - preservations.push({ - type: "region", - fromStart: from.firstNode(), fromEnd: from.lastNode(), - newRange: to - }); - - nodePreserveRoots.push({ context: from.containerNode(), preserve: to.preserve, - fromRange: from, toRange: to }); - }); - - // Consider landmarks enclosing the updated region - var walk = existingRange; - while ((walk = findParentOfType(Spark._ANNOTATION_LANDMARK, walk))) - nodePreserveRoots.push({ context: walk.containerNode(), preserve: walk.preserve, - fromRange: existingRange, toRange: newRange}); - - var visitLabeledNodes = function (context, clipRange, nodeLabeler, selector, func) { - var nodes = DomUtils.findAllClipped( - context, selector, clipRange.firstNode(), clipRange.lastNode()); - - _.each(nodes, function (n) { - var label = nodeLabeler(n); - label && func(n, label); - }); - }; - - // Find the old incarnation of each of the preserved nodes - _.each(nodePreserveRoots, function (root) { - root.fromNodesByLabel = {}; - _.each(root.preserve, function (nodeLabeler, selector) { - root.fromNodesByLabel[selector] = {}; - visitLabeledNodes(root.context, root.fromRange, nodeLabeler, selector, function (n, label) { - root.fromNodesByLabel[selector][label] = n; - }); - }); - }); - - // Temporarily put newRange into the document so that we can do - // properly contextualized selector queries against it. - // - // Create a temporary range around newRange, and also around any enclosing - // ranges that happen to also start and end on those nodes. It is ok - // to temporarily put these in the document as well, because CSS selectors - // don't care and we will put them back. `tempRange` will hold our place - // in the tree `newRange` came from. - var tempRange = new LiveRange(Spark._TAG, newRange.firstNode(), newRange.lastNode()); - var commentFrag = document.createDocumentFragment(); - commentFrag.appendChild(document.createComment("")); - var newRangeFrag = tempRange.replace_contents(commentFrag); - // `wrapperRange` will mark where we inserted newRange into the document. - var wrapperRange = new LiveRange(Spark._TAG, newRangeFrag); - existingRange.insert_before(newRangeFrag); - - - _.each(nodePreserveRoots, function (root) { - _.each(root.preserve, function (nodeLabeler, selector) { - visitLabeledNodes(root.context, root.toRange, nodeLabeler, selector, function (n, label) { - var match = root.fromNodesByLabel[selector][label]; - if (match) { - preservations.push({ type: "node", from: match, to: n }); - root.fromNodesByLabel[selector][label] = null; - } - }); - }); - }); - - - - // Extraction is legal because we're just taking the document - // back to the state it was in before insertBefore. - var extractedFrag = wrapperRange.extract(); - wrapperRange.destroy(); - tempRange.replace_contents(extractedFrag); - tempRange.destroy(); - - return preservations; -}; - // Find all the landmarks in `range` and let them know that they are // now onscreen. If it's their first time being onscreen, they need to // have their `create` callback called. And they need `render` whether