diff --git a/src/morphdom.js b/src/morphdom.js index f978372..64cd697 100644 --- a/src/morphdom.js +++ b/src/morphdom.js @@ -236,6 +236,27 @@ export default function morphdomFactory(morphAttrs) { } } + function morphText(fromNode, toNode) { + var fromText = fromNode.nodeValue; + var toText = toNode.nodeValue; + + if (fromText === toText) { + return; + } + + // Handle incremental update case + if (fromNode.nodeType === TEXT_NODE && toText.startsWith(fromText)) { + var appendedText = toText.substring(fromText.length); + fromNode.after(appendedText); + fromNode.parentNode.normalize(); + return; + } + + // Simply update nodeValue on the original node to + // change the text value + fromNode.nodeValue = toText; + } + function morphChildren(fromEl, toEl) { var skipFrom = skipFromChildren(fromEl, toEl); var curToNodeChild = toEl.firstChild; @@ -336,12 +357,7 @@ export default function morphdomFactory(morphAttrs) { } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) { // Both nodes being compared are Text or Comment nodes isCompatible = true; - // Simply update nodeValue on the original node to - // change the text value - if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) { - curFromNodeChild.nodeValue = curToNodeChild.nodeValue; - } - + morphText(curFromNodeChild, curToNodeChild); } } @@ -426,9 +442,7 @@ export default function morphdomFactory(morphAttrs) { } } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node if (toNodeType === morphedNodeType) { - if (morphedNode.nodeValue !== toNode.nodeValue) { - morphedNode.nodeValue = toNode.nodeValue; - } + morphText(morphedNode, toNode); return morphedNode; } else { diff --git a/test/browser/test.js b/test/browser/test.js index 146d4be..a4fcf4e 100644 --- a/test/browser/test.js +++ b/test/browser/test.js @@ -1657,6 +1657,84 @@ describe('morphdom' , function() { expect(noUpdateParentBefore.isSameNode(noUpdateBefore.parentNode)).to.equal(false); }); + it('should preserve text selection during incremental updates', () => { + // Initial setup + var fromEl = document.createElement('div'); + var initialText = 'Hello world'; + fromEl.textContent = initialText; + document.body.appendChild(fromEl); + + // Create and set selection + var selection = window.getSelection(); + var range = document.createRange(); + range.setStart(fromEl.firstChild, 0); + range.setEnd(fromEl.firstChild, 5); // Select "Hello" + selection.removeAllRanges(); + selection.addRange(range); + + // Create target node with incremental update + var toEl = document.createElement('div'); + toEl.textContent = 'Hello world, how are you?'; + + // Apply morphdom with incremental update option + morphdom(fromEl, toEl); + + // Verify selection is preserved + var updatedSelection = window.getSelection(); + expect(updatedSelection.toString()).to.equal('Hello'); + expect(fromEl.textContent).to.equal('Hello world, how are you?'); + }); + + it('should handle multiple incremental updates correctly', () => { + var fromEl = document.createElement('div'); + fromEl.textContent = 'Start'; + document.body.appendChild(fromEl); + + // Create and set selection + var selection = window.getSelection(); + var range = document.createRange(); + range.setStart(fromEl.firstChild, 0); + range.setEnd(fromEl.firstChild, 5); + selection.removeAllRanges(); + selection.addRange(range); + + // Perform multiple updates + var updates = [ + 'Start with', + 'Start with more', + 'Start with more text', + 'Start with more text!!!', + ]; + + updates.forEach(text => { + var toEl = document.createElement('div'); + toEl.textContent = text; + + morphdom(fromEl, toEl); + + // Verify text content after each update + expect(fromEl.textContent).to.equal(text); + }); + + // Verify final selection + expect(window.getSelection().toString()).to.equal('Start'); + }); + + it('should properly normalize DOM after incremental updates', () => { + var fromEl = document.createElement('div'); + fromEl.textContent = 'Initial'; + + var toEl = document.createElement('div'); + toEl.textContent = 'Initial text with more content'; + + morphdom(fromEl, toEl); + + // Verify that we have a single text node after normalization + expect(fromEl.childNodes.length).to.equal(1); + expect(fromEl.childNodes[0].nodeType).to.equal(3); // TEXT_NODE + expect(fromEl.textContent).to.equal('Initial text with more content'); + }); + xit('should reuse DOM element with matching ID and class name (2)', function() { // NOTE: This test is currently failing. We need to improve the special case code // for handling incompatible root nodes. diff --git a/test/fixtures/autotest/incremental-text/from.html b/test/fixtures/autotest/incremental-text/from.html new file mode 100644 index 0000000..bc090af --- /dev/null +++ b/test/fixtures/autotest/incremental-text/from.html @@ -0,0 +1,5 @@ +test1 +test2 +test3 + +test4 \ No newline at end of file diff --git a/test/fixtures/autotest/incremental-text/to.html b/test/fixtures/autotest/incremental-text/to.html new file mode 100644 index 0000000..5415a79 --- /dev/null +++ b/test/fixtures/autotest/incremental-text/to.html @@ -0,0 +1,5 @@ +test1 FOO +test2 BAR +test3 FOO +BAR +test4 FOO \ No newline at end of file