diff --git a/modules/ve2/dm/ve.dm.Transaction.js b/modules/ve2/dm/ve.dm.Transaction.js index c2a49ed07d..a813efc272 100644 --- a/modules/ve2/dm/ve.dm.Transaction.js +++ b/modules/ve2/dm/ve.dm.Transaction.js @@ -72,13 +72,23 @@ ve.dm.Transaction.newFromRemoval = function( doc, range ) { return tx; } // Select nodes and validate selection - var selection = doc.selectNodes( range, 'leaves' ); + var selection = doc.selectNodes( range, 'leaves' ), + nodeRange; if ( selection.length === 0 ) { // Empty selection? Something is wrong! throw 'Invalid range, can not remove from ' + range.start + ' to ' + range.end; } // Decide whether to merge or strip if ( selection[0].node.canBeMergedWith( selection[selection.length - 1].node ) ) { + // If only one node was selected, ignore anything past this node + if ( selection.length === 1 ) { + // Include the parent's wrapping (if any - there should always be, but let's be safe) + wrapping = selection[0].node.getParent().isWrapped() ? 1 : 0; + // Only reduces the range to cover the selected node if it's shorter + range.start = Math.max( range.start, selection[0].nodeRange.start - wrapping ); + // Only reduces the range to cover the selected node if it's shorter + range.end = Math.min( range.end, selection[0].nodeRange.end + wrapping ); + } // Retain to the start of the range if ( range.start > 0 ) { tx.pushRetain( range.start ); @@ -90,8 +100,7 @@ ve.dm.Transaction.newFromRemoval = function( doc, range ) { tx.pushRetain( data.length - range.end ); } } else { - var offset = 0, - nodeRange; + var offset = 0; for ( var i = 0; i < selection.length; i++ ) { nodeRange = selection[i].nodeRange; // Retain up to where the next removal starts diff --git a/tests/ve2/dm/ve.dm.Node.test.js b/tests/ve2/dm/ve.dm.Node.test.js index a2843afdca..4d48955177 100644 --- a/tests/ve2/dm/ve.dm.Node.test.js +++ b/tests/ve2/dm/ve.dm.Node.test.js @@ -96,3 +96,14 @@ test( 'detach', 2, function() { strictEqual( node1.getRoot(), node1 ); } ); +test( 'canBeMergedWith', 4, function() { + var node1 = new ve.dm.LeafNodeStub(), + node2 = new ve.dm.BranchNodeStub( [node1] ), + node3 = new ve.dm.BranchNodeStub( [node2] ), + node4 = new ve.dm.LeafNodeStub(), + node5 = new ve.dm.BranchNodeStub( [node4] ); + strictEqual( node3.canBeMergedWith( node5 ), true, 'same level, same type' ); + strictEqual( node2.canBeMergedWith( node5 ), false, 'different level, same type' ); + strictEqual( node2.canBeMergedWith( node1 ), false, 'different level, different type' ); + strictEqual( node2.canBeMergedWith( node4 ), false, 'same level, different type' ); +} ); diff --git a/tests/ve2/dm/ve.dm.Transaction.test.js b/tests/ve2/dm/ve.dm.Transaction.test.js index 83ff99197a..e032c1a36a 100644 --- a/tests/ve2/dm/ve.dm.Transaction.test.js +++ b/tests/ve2/dm/ve.dm.Transaction.test.js @@ -150,6 +150,23 @@ test( 'newFromRemoval', function() { { 'type': 'retain', 'length': 19 } ] }, + 'extra openings': { + 'args': [doc, new ve.Range( 0, 7 )], + 'ops': [ + { + 'type': 'replace', + 'remove': [ + { 'type': 'heading', 'attributes': { 'level': 1 } }, + 'a', + ['b', { '{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' } }], + ['c', { '{"type":"textStyle/italic"}': { 'type': 'textStyle/italic' } }], + { 'type': '/heading' } + ], + 'insert': [] + }, + { 'type': 'retain', 'length': 54 } + ] + }, 'last element': { 'args': [doc, new ve.Range( 56, 59 )], 'ops': [ @@ -161,6 +178,18 @@ test( 'newFromRemoval', function() { } ] }, + 'extra closings': { + 'args': [doc, new ve.Range( 30, 37 )], + 'ops': [ + { 'type': 'retain', 'length': 36 }, + { + 'type': 'replace', + 'remove': ['h'], + 'insert': [] + }, + { 'type': 'retain', 'length': 22 } + ] + }, 'merge last two elements': { 'args': [doc, new ve.Range( 55, 57 )], 'ops': [