diff --git a/modules/ve/dm/ve.dm.SurfaceFragment.js b/modules/ve/dm/ve.dm.SurfaceFragment.js index 5dd1f0b1dc..5cb587c9f6 100644 --- a/modules/ve/dm/ve.dm.SurfaceFragment.js +++ b/modules/ve/dm/ve.dm.SurfaceFragment.js @@ -662,6 +662,13 @@ ve.dm.SurfaceFragment.prototype.wrapAllNodes = function ( wrapper ) { /** * Unwrap nodes in the fragment out of one or more elements. * + * Unwrap only removes elements from inside the fragment. + * + * Example: + * // fragment is a selection of:
text
text
+ * * @method * @param {number} depth Number of nodes to unwrap * @chainable @@ -671,7 +678,7 @@ ve.dm.SurfaceFragment.prototype.unwrapAllNodes = function ( depth ) { if ( !this.surface ) { return this; } - var i, tx, newRange, wrapper = [], + var i, tx, newRange, unwrapper = [], innerRange = new ve.Range( this.range.start + depth, this.range.end - depth ); if ( this.range.end - this.range.start < depth * 2 ) { @@ -679,12 +686,12 @@ ve.dm.SurfaceFragment.prototype.unwrapAllNodes = function ( depth ) { } for ( i = 0; i < depth; i++ ) { - wrapper.push( this.surface.getDocument().data[this.range.start + i] ); + unwrapper.push( this.surface.getDocument().data[this.range.start + i] ); } newRange = new ve.Range( this.range.start, this.range.end - ( depth * 2 ) ); - tx = ve.dm.Transaction.newFromWrap( this.document, innerRange, wrapper, [], [], [] ); + tx = ve.dm.Transaction.newFromWrap( this.document, innerRange, unwrapper, [], [], [] ); this.surface.change( tx, !this.noAutoSelect && newRange ); this.range = newRange; @@ -705,12 +712,30 @@ ve.dm.SurfaceFragment.prototype.unwrapAllNodes = function ( depth ) { * @param {Object} [wrapper.attributes] Attributes of wrapper * @chainable */ -ve.dm.SurfaceFragment.prototype.rewrapAllNodes = function () { +ve.dm.SurfaceFragment.prototype.rewrapAllNodes = function ( depth, wrapper ) { // Handle null fragment if ( !this.surface ) { return this; } - // TODO: Implement + var i, tx, newRange, unwrapper = [], + depthChange = wrapper.length - depth, + innerRange = new ve.Range( this.range.start + depth, this.range.end - depth ); + + if ( this.range.end - this.range.start < depth * 2 ) { + throw new Error( 'cannot unwrap by greater depth than maximum theoretical depth of selection' ); + } + + for ( i = 0; i < depth; i++ ) { + unwrapper.push( this.surface.getDocument().data[this.range.start + i] ); + } + + newRange = new ve.Range( this.range.start, this.range.end + ( depthChange * 2 ) ); + + tx = ve.dm.Transaction.newFromWrap( this.document, innerRange, unwrapper, wrapper, [], [] ); + this.surface.change( tx, !this.noAutoSelect && newRange ); + + this.range = newRange; + return this; }; diff --git a/modules/ve/dm/ve.dm.Transaction.js b/modules/ve/dm/ve.dm.Transaction.js index bb55b68339..1884740e46 100644 --- a/modules/ve/dm/ve.dm.Transaction.js +++ b/modules/ve/dm/ve.dm.Transaction.js @@ -459,7 +459,7 @@ ve.dm.Transaction.newFromContentBranchConversion = function ( doc, range, type, * * @param {ve.dm.Document} doc Document to generate a transaction for * @param {ve.Range} range Range to wrap/unwrap/replace around - * @param {Array} unwrapOuter Oopening elements to unwrap. These must be immediately *outside* the range. + * @param {Array} unwrapOuter Opening elements to unwrap. These must be immediately *outside* the range. * @param {Array} wrapOuter Opening elements to wrap around the range. * @param {Array} unwrapEach Opening elements to unwrap from each top-level element in the range. * @param {Array} wrapEach Opening elements to wrap around each top-level element in the range. diff --git a/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js b/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js index 2e124f568d..83e0f6b404 100644 --- a/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js +++ b/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js @@ -255,6 +255,69 @@ QUnit.test( 'wrapAllNodes/unwrapAllNodes', 10, function ( assert ) { ); }); +QUnit.test( 'rewrapAllNodes', 6, function ( assert ) { + var expectedData, + doc = new ve.dm.Document( ve.copyArray( ve.dm.example.data ) ), + surface = new ve.dm.Surface( doc ), + fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 5, 37 ) ), + expectedDoc = new ve.dm.Document( ve.copyArray( ve.dm.example.data ) ), + expectedSurface = new ve.dm.Surface( expectedDoc ), + expectedFragment = new ve.dm.SurfaceFragment( expectedSurface, new ve.Range( 5, 37 ) ), + created = { 'changed': { 'created' : 1 } }; + + // Compare a rewrap operation with its equivalent unwrap + wrap + // This type of test can only exist if the intermediate state is valid + fragment.rewrapAllNodes( + 4, + [{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, { 'type': 'listItem' }] + ); + expectedFragment.unwrapAllNodes( 4 ); + expectedFragment.wrapAllNodes( + [{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, { 'type': 'listItem' }] + ); + assert.deepEqual( + doc.getData(), + expectedDoc.getData(), + 'rewrapping multiple nodes via a valid intermediate state produces the same document as unwrapping then wrapping' + ); + assert.deepEqual( fragment.getRange(), expectedFragment.getRange(), 'new range contains rewrapping elements' ); + + // Reverse of first test + fragment.rewrapAllNodes( + 2, + [ + { 'type': 'table', }, + { 'type': 'tableSection', 'attributes': { 'style': 'body' } }, + { 'type': 'tableRow' }, + { 'type': 'tableCell', 'attributes': { 'style': 'data' } } + ] + ); + + expectedData = ve.copyArray( ve.dm.example.data ); + expectedData[5].internal = created; + expectedData[6].internal = created; + expectedData[7].internal = created; + expectedData[8].internal = created; + assert.deepEqual( + doc.getData(), + expectedData, + 'rewrapping multiple nodes via a valid intermediate state produces the same document as unwrapping then wrapping' + ); + assert.deepEqual( fragment.getRange(), new ve.Range( 5, 37 ), 'new range contains rewrapping elements' ); + + // Rewrap a heading as a paragraph + // The intermediate stage (plain text attached to the document) would be invalid + // if performed as an unwrap and a wrap + fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 0, 5 ) ); + fragment.rewrapAllNodes( 1, [ { 'type': 'paragraph' } ] ); + + expectedData.splice( 0, 1, { 'type': 'paragraph', 'internal': created } ); + expectedData.splice( 4, 1, { 'type': '/paragraph' } ); + + assert.deepEqual( doc.getData(), expectedData, 'rewrapping a heading as a paragraph' ); + assert.deepEqual( fragment.getRange(), new ve.Range( 0, 5 ), 'new range contains rewrapping elements' ); +}); + function runIsolateTest( assert, range, expected, label ) { var doc = new ve.dm.Document( ve.copyArray( ve.dm.example.isolationData ) ), surface = new ve.dm.Surface( doc ),