From feaa422495364b002897e8ececbd18b08102fb8e Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Thu, 21 Feb 2013 11:20:43 -0800 Subject: [PATCH] (bug 45423) Create SurfaceFragment.isolate method This method will take a selection of siblings and ensure they are the only chlidren in their first parent which can be placed anywhere (for example the first parent of a tableCell which can be placed anywhere is a table, and for a listItem is a list). The method ensures no redundant empty tags are created, so if the selection encompasses all siblings then no action is taken. Also in this commit are two test cases run against ve.dm.example.isolationData. Change-Id: I783bd5ecd9d43d61f9b2685985409b4d746cbe94 --- modules/ve/dm/ve.dm.SurfaceFragment.js | 85 +++++++++++++++++++ .../ve/test/dm/ve.dm.SurfaceFragment.test.js | 47 +++++++++- 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/modules/ve/dm/ve.dm.SurfaceFragment.js b/modules/ve/dm/ve.dm.SurfaceFragment.js index fd9e0956c0..c2080992ac 100644 --- a/modules/ve/dm/ve.dm.SurfaceFragment.js +++ b/modules/ve/dm/ve.dm.SurfaceFragment.js @@ -690,3 +690,88 @@ ve.dm.SurfaceFragment.prototype.rewrapAllNodes = function () { // TODO: Implement return this; }; + +/** + * Isolates the nodes in a fragment. + * + * The node selection is expanded to siblings and then these are isolated such that they are the + * sole children of a parent element which can be placed anywhere. + * + * @method + * @returns {ve.dm.SurfaceFragment} This fragment + */ +ve.dm.SurfaceFragment.prototype.isolate = function () { + // Handle null fragment + if ( !this.surface ) { + return this; + } + var nodes, startSplitNode, endSplitNode, tx, + startOffset, endOffset, + startSplitRequired = false, + endSplitRequired = false, + startSplitNodes = [], + endSplitNodes = [], + fragment = this; + + function createSplits( splitNodes, insertBefore ) { + var i, length, + startOffsetChange = 0, endOffsetChange = 0, data = []; + for ( i = 0, length = splitNodes.length; i < length; i++ ) { + data.unshift( { 'type': '/' + splitNodes[i].type } ); + data.push( splitNodes[i].getClonedElement() ); + + if ( insertBefore ) { + startOffsetChange += 2; + endOffsetChange += 2; + } + } + + tx = ve.dm.Transaction.newFromInsertion( fragment.document, insertBefore ? startOffset : endOffset, data ); + fragment.surface.change( tx, !fragment.noAutoSelect && tx.translateRange( fragment.range ) ); + + startOffset += startOffsetChange; + endOffset += endOffsetChange; + } + + nodes = this.document.selectNodes( this.range, 'siblings' ); + + // Find start split point, if required + startSplitNode = nodes[0].node; + startOffset = startSplitNode.getOuterRange().start; + while ( startSplitNode.constructor.static.parentNodeTypes !== null ) { + if ( startSplitNode.parent.indexOf( startSplitNode ) > 0 ) { + startSplitRequired = true; + } + startSplitNode = startSplitNode.parent; + if ( startSplitRequired ) { + startSplitNodes.unshift(startSplitNode); + } else { + startOffset = startSplitNode.getOuterRange().start; + } + } + + // Find end split point, if required + endSplitNode = nodes[nodes.length - 1].node; + endOffset = endSplitNode.getOuterRange().end; + while ( endSplitNode.constructor.static.parentNodeTypes !== null ) { + if ( endSplitNode.parent.indexOf( endSplitNode ) < endSplitNode.parent.getChildren().length - 1 ) { + endSplitRequired = true; + } + endSplitNode = endSplitNode.parent; + if ( endSplitRequired ) { + endSplitNodes.unshift(endSplitNode); + } else { + endOffset = endSplitNode.getOuterRange().end; + } + } + + if ( startSplitRequired ) { + createSplits( startSplitNodes, true ); + } + + if ( endSplitRequired ) { + createSplits( endSplitNodes, false ); + } + + return this; +}; diff --git a/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js b/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js index a685386797..88c2a5bb30 100644 --- a/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js +++ b/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js @@ -7,7 +7,7 @@ QUnit.module( 've.dm.SurfaceFragment' ); -// Tests +/* Tests */ QUnit.test( 'constructor', 8, function ( assert ) { var doc = new ve.dm.Document( ve.copyArray( ve.dm.example.data ) ), @@ -225,3 +225,48 @@ QUnit.test( 'wrapAllNodes', 2, function ( assert ) { 'wrapping nodes can add multiple levels of wrapping to a single element' ); } ); + +function runIsolateTest( assert, range, expected, label ) { + var doc = new ve.dm.Document( ve.copyArray( ve.dm.example.isolationData ) ), + surface = new ve.dm.Surface( doc ), + fragment = new ve.dm.SurfaceFragment( surface, range ), + data; + + data = ve.copyArray( doc.getFullData() ); + fragment.isolate(); + expected( data ); + + assert.deepEqual( doc.getFullData(), data, label ); +} + +QUnit.test( 'isolate', 2, function ( assert ) { + var rebuilt = { 'changed': { 'rebuilt': 1 } }, + created = { 'changed': { 'created': 1 } }, + createdAndRebuilt = { 'changed': { 'created': 1, 'rebuilt': 1 } }; + + runIsolateTest( assert, new ve.Range( 11, 21 ), function( data ) { + data[0].internal = rebuilt; + data.splice( 11, 0, { 'type': '/list' }, { 'type': 'list', 'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt }); + data.splice( 23, 0, { 'type': '/list' }, { 'type': 'list', 'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt }); + }, 'isolating list item "Item 2"'); + + runIsolateTest( assert, new ve.Range( 88, 108 ), function( data ) { + data[75].internal = rebuilt; + data[76].internal = rebuilt; + data[77].internal = rebuilt; + data.splice( 88, 0, + { 'type': '/tableRow' }, + { 'type': '/tableSection' }, + { 'type': '/table' }, + { 'type': 'table', 'internal': createdAndRebuilt }, + { 'type': 'tableSection', 'attributes': { 'style': 'body' }, 'internal': createdAndRebuilt }, + { 'type': 'tableRow', 'internal': created } + ); + data.splice( 115, 0, + { 'type': '/tableSection' }, + { 'type': '/table' }, + { 'type': 'table', 'internal': createdAndRebuilt }, + { 'type': 'tableSection', 'attributes': { 'style': 'body' }, 'internal': createdAndRebuilt } + ); + }, 'isolating table cells "Cell 2" & "Cell 3"'); +} );