mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-09-24 18:58:42 +00:00
The resurrection
By removing the transaction listeners from surface fragments we no longer have to make sure they are always manually destroyed. In order to retain the functionality of having fragments update with transactions elsewhere we keep a pointer to a place in the new complete history stack in the surface. The complete history stack records all transactions, even undone ones. Whenever getRange is called we replay all transactions in the complete history (in the correct order) since the fragment was last updated. Also in this commit: * Updated Format/IndentationAction to test undo(). This increases coverage of surface fragment behaviour. * .range is always accessed by .getRange now, although as an optimisation we can use the noCopy mode when we a sure the returned range will not be modified. * Added undo test to .update (previously .onTransact) Bug: 47343 Change-Id: I9e9818da1baa8319a3002f6d74fd1aad6732a8f5
This commit is contained in:
parent
1d27c3cd26
commit
8b09dd7650
|
@ -42,7 +42,7 @@ ve.AnnotationAction.static.methods = ['set', 'clear', 'toggle', 'clearAll'];
|
|||
* @param {Object} [data] Additional annotation data
|
||||
*/
|
||||
ve.AnnotationAction.prototype.set = function ( name, data ) {
|
||||
this.surface.getModel().getFragment().annotateContent( 'set', name, data ).destroy();
|
||||
this.surface.getModel().getFragment().annotateContent( 'set', name, data );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -53,7 +53,7 @@ ve.AnnotationAction.prototype.set = function ( name, data ) {
|
|||
* @param {Object} [data] Additional annotation data
|
||||
*/
|
||||
ve.AnnotationAction.prototype.clear = function ( name, data ) {
|
||||
this.surface.getModel().getFragment().annotateContent( 'clear', name, data ).destroy();
|
||||
this.surface.getModel().getFragment().annotateContent( 'clear', name, data );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -68,11 +68,9 @@ ve.AnnotationAction.prototype.clear = function ( name, data ) {
|
|||
*/
|
||||
ve.AnnotationAction.prototype.toggle = function ( name, data ) {
|
||||
var fragment = this.surface.getModel().getFragment();
|
||||
fragment
|
||||
.annotateContent(
|
||||
fragment.getAnnotations().hasAnnotationWithName( name ) ? 'clear' : 'set', name, data
|
||||
)
|
||||
.destroy();
|
||||
fragment.annotateContent(
|
||||
fragment.getAnnotations().hasAnnotationWithName( name ) ? 'clear' : 'set', name, data
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -91,7 +89,6 @@ ve.AnnotationAction.prototype.clearAll = function () {
|
|||
for ( i = 0, len = arr.length; i < len; i++ ) {
|
||||
fragment.annotateContent( 'clear', arr[i].name, arr[i].data );
|
||||
}
|
||||
fragment.destroy();
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
|
|
@ -42,7 +42,7 @@ ve.ContentAction.static.methods = ['insert', 'remove', 'select'];
|
|||
* @param {boolean} annotate Content should be automatically annotated to match surrounding content
|
||||
*/
|
||||
ve.ContentAction.prototype.insert = function ( content, annotate ) {
|
||||
this.surface.getModel().getFragment().insertContent( content, annotate ).destroy();
|
||||
this.surface.getModel().getFragment().insertContent( content, annotate );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -51,7 +51,7 @@ ve.ContentAction.prototype.insert = function ( content, annotate ) {
|
|||
* @method
|
||||
*/
|
||||
ve.ContentAction.prototype.remove = function () {
|
||||
this.surface.getModel().getFragment().removeContent().destroy();
|
||||
this.surface.getModel().getFragment().removeContent();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -65,10 +65,9 @@ ve.FormatAction.prototype.convert = function ( type, attributes ) {
|
|||
}
|
||||
|
||||
for ( i = 0, length = fragments.length; i < length; i++ ) {
|
||||
fragments[i].isolateAndUnwrap( type ).destroy();
|
||||
fragments[i].isolateAndUnwrap( type );
|
||||
}
|
||||
selection = fragmentForSelection.getRange();
|
||||
fragmentForSelection.destroy();
|
||||
|
||||
txs = ve.dm.Transaction.newFromContentBranchConversion( doc, selection, type, attributes );
|
||||
surfaceModel.change( txs, selection );
|
||||
|
|
|
@ -65,10 +65,9 @@ ve.IndentationAction.prototype.increase = function () {
|
|||
this.indentListItem(
|
||||
documentModel.getNodeFromOffset( fragments[i].getRange().start + 1 )
|
||||
);
|
||||
fragments[i].destroy();
|
||||
}
|
||||
|
||||
selected.select().destroy();
|
||||
selected.select();
|
||||
|
||||
return increased;
|
||||
};
|
||||
|
@ -104,10 +103,9 @@ ve.IndentationAction.prototype.decrease = function () {
|
|||
this.unindentListItem(
|
||||
documentModel.getNodeFromOffset( fragments[i].getRange().start + 1 )
|
||||
);
|
||||
fragments[i].destroy();
|
||||
}
|
||||
|
||||
selected.select().destroy();
|
||||
selected.select();
|
||||
|
||||
return decreased;
|
||||
};
|
||||
|
@ -301,7 +299,6 @@ ve.IndentationAction.prototype.unindentListItem = function ( listItem ) {
|
|||
);
|
||||
surfaceModel.change( tx );
|
||||
}
|
||||
fragment.destroy();
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
|
|
@ -94,7 +94,7 @@ ve.ce.ImageNode.prototype.onClick = function ( e ) {
|
|||
[ selectionRange, nodeRange ], selectionRange.from > nodeRange.from
|
||||
) :
|
||||
nodeRange
|
||||
).select().destroy();
|
||||
).select();
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
|
|
@ -324,12 +324,11 @@ ve.ce.Surface.prototype.onDocumentDrop = function ( e ) {
|
|||
nodeData = originFragment.getData();
|
||||
|
||||
// Remove node from old location (auto-updates targetFragment's range)
|
||||
originFragment.removeContent().destroy();
|
||||
originFragment.removeContent();
|
||||
|
||||
// Re-insert node at new location and re-select it
|
||||
targetFragment.insertContent( nodeData );
|
||||
targetFragment.adjustRange( -nodeData.length, 0 ).select().destroy();
|
||||
targetFragment.destroy();
|
||||
targetFragment.adjustRange( -nodeData.length, 0 ).select();
|
||||
}, this ) );
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ ve.dm.Surface = function VeDmSurface( doc ) {
|
|||
this.selectedNodes = {};
|
||||
this.smallStack = [];
|
||||
this.bigStack = [];
|
||||
this.completeHistory = [];
|
||||
this.undoIndex = 0;
|
||||
this.historyTrackingInterval = null;
|
||||
this.insertionAnnotations = new ve.dm.AnnotationSet( this.documentModel.getStore() );
|
||||
|
@ -286,6 +287,10 @@ ve.dm.Surface.prototype.change = function ( transactions, selection ) {
|
|||
this.bigStack = this.bigStack.slice( 0, this.bigStack.length - this.undoIndex );
|
||||
this.undoIndex = 0;
|
||||
this.smallStack.push( transactions[i] );
|
||||
this.completeHistory.push( {
|
||||
'undo': false,
|
||||
'transaction': transactions[i]
|
||||
} );
|
||||
this.documentModel.commit( transactions[i] );
|
||||
}
|
||||
}
|
||||
|
@ -411,7 +416,11 @@ ve.dm.Surface.prototype.undo = function () {
|
|||
for ( i = item.stack.length - 1; i >= 0; i-- ) {
|
||||
transaction = item.stack[i];
|
||||
selection = transaction.translateRange( selection, true );
|
||||
this.documentModel.rollback( item.stack[i] );
|
||||
this.completeHistory.push( {
|
||||
'undo': true,
|
||||
'transaction': transaction
|
||||
} );
|
||||
this.documentModel.rollback( transaction );
|
||||
}
|
||||
this.emit( 'unlock' );
|
||||
this.emit( 'history' );
|
||||
|
@ -420,6 +429,23 @@ ve.dm.Surface.prototype.undo = function () {
|
|||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the length of the complete history stack. This is also the current pointer.
|
||||
* @returns {number} Length of the complete history stack
|
||||
*/
|
||||
ve.dm.Surface.prototype.getCompleteHistoryLength = function () {
|
||||
return this.completeHistory.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all the items in the complete history stack since a specified pointer.
|
||||
* @param {number} pointer Pointer from where to start the slice
|
||||
* @returns {Array} Array of transaction objects with undo flag
|
||||
*/
|
||||
ve.dm.Surface.prototype.getCompleteHistorySince = function ( pointer ) {
|
||||
return this.completeHistory.slice( pointer );
|
||||
};
|
||||
|
||||
/**
|
||||
* Step forwards in history.
|
||||
*
|
||||
|
@ -434,7 +460,7 @@ ve.dm.Surface.prototype.redo = function () {
|
|||
if ( !this.enabled ) {
|
||||
return;
|
||||
}
|
||||
var selection, item, i;
|
||||
var item, i, transaction, selection;
|
||||
this.breakpoint();
|
||||
|
||||
if ( this.undoIndex > 0 && this.bigStack[this.bigStack.length - this.undoIndex] ) {
|
||||
|
@ -442,7 +468,12 @@ ve.dm.Surface.prototype.redo = function () {
|
|||
item = this.bigStack[this.bigStack.length - this.undoIndex];
|
||||
selection = item.selection;
|
||||
for ( i = 0; i < item.stack.length; i++ ) {
|
||||
this.documentModel.commit( item.stack[i] );
|
||||
transaction = item.stack[i];
|
||||
this.completeHistory.push( {
|
||||
'undo': false,
|
||||
'transaction': transaction
|
||||
} );
|
||||
this.documentModel.commit( transaction );
|
||||
}
|
||||
this.undoIndex--;
|
||||
this.emit( 'unlock' );
|
||||
|
|
|
@ -21,26 +21,23 @@ ve.dm.SurfaceFragment = function VeDmSurfaceFragment( surface, range, noAutoSele
|
|||
}
|
||||
|
||||
// Properties
|
||||
this.range = range && range instanceof ve.Range ? range : surface.getSelection();
|
||||
this.surface = surface;
|
||||
this.setRange( range && range instanceof ve.Range ? range : surface.getSelection() );
|
||||
// Short-circuit for invalid range null fragment
|
||||
if ( !this.range ) {
|
||||
return this;
|
||||
}
|
||||
this.surface = surface;
|
||||
this.document = surface.getDocument();
|
||||
this.noAutoSelect = !!noAutoSelect;
|
||||
this.onTransactHandler = ve.bind( this.onTransact, this );
|
||||
|
||||
// Events
|
||||
surface.on( 'transact', this.onTransactHandler );
|
||||
|
||||
// Initialization
|
||||
var length = this.document.data.getLength();
|
||||
this.range = new ve.Range(
|
||||
this.setRange( new ve.Range(
|
||||
// Clamp range to valid document offsets
|
||||
Math.min( Math.max( this.range.from, 0 ), length ),
|
||||
Math.min( Math.max( this.range.to, 0 ), length )
|
||||
);
|
||||
) );
|
||||
};
|
||||
|
||||
/* Static Properties */
|
||||
|
@ -55,16 +52,19 @@ ve.dm.SurfaceFragment.static = {};
|
|||
/* Methods */
|
||||
|
||||
/**
|
||||
* Handle transactions being processed on the document.
|
||||
*
|
||||
* This keeps the range of the fragment valid, even while other transactions are being processed
|
||||
* Update range based on un-applied transactions in the surface.
|
||||
*
|
||||
* @method
|
||||
* @param {ve.dm.Transaction[]} txs Transactions that have just been processed
|
||||
*/
|
||||
ve.dm.SurfaceFragment.prototype.onTransact = function ( txs ) {
|
||||
for ( var i = 0; i < txs.length; i++ ) {
|
||||
this.range = txs[i].translateRange( this.range );
|
||||
ve.dm.SurfaceFragment.prototype.update = function () {
|
||||
var i, length, txs;
|
||||
// Small optimisation: check history pointer is in the past
|
||||
if ( this.historyPointer < this.getSurface().getCompleteHistoryLength() ) {
|
||||
txs = this.getSurface().getCompleteHistorySince( this.historyPointer );
|
||||
for ( i = 0, length = txs.length; i < length; i++ ) {
|
||||
this.range = txs[i].transaction.translateRange( this.range, txs[i].undo );
|
||||
this.historyPointer++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -91,11 +91,26 @@ ve.dm.SurfaceFragment.prototype.getDocument = function () {
|
|||
/**
|
||||
* Get the range of the fragment within the surface.
|
||||
*
|
||||
* This method also calls update to make sure the range returned is current.
|
||||
*
|
||||
* @method
|
||||
* @param {boolean} noCopy Return the range by reference, not a copy
|
||||
* @returns {ve.Range} Surface range
|
||||
*/
|
||||
ve.dm.SurfaceFragment.prototype.getRange = function () {
|
||||
return this.range.clone();
|
||||
ve.dm.SurfaceFragment.prototype.getRange = function ( noCopy ) {
|
||||
this.update();
|
||||
return noCopy ? this.range : this.range.clone();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the range of the transaction and reset the history pointer.
|
||||
*
|
||||
* @method
|
||||
* @param {ve.Range} range New range
|
||||
*/
|
||||
ve.dm.SurfaceFragment.prototype.setRange = function ( range ) {
|
||||
this.historyPointer = this.getSurface().getCompleteHistoryLength();
|
||||
this.range = range;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -108,21 +123,6 @@ ve.dm.SurfaceFragment.prototype.isNull = function () {
|
|||
return this.surface === undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys fragment, removing event handlers and object references, leaving the fragment null.
|
||||
*
|
||||
* Call this whenever you are done using a fragment.
|
||||
*
|
||||
* @method
|
||||
*/
|
||||
ve.dm.SurfaceFragment.prototype.destroy = function () {
|
||||
if ( this.surface ) {
|
||||
this.surface.removeListener( 'transact', this.onTransactHandler );
|
||||
this.surface = null;
|
||||
this.document = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a new fragment with an adjusted position
|
||||
*
|
||||
|
@ -138,7 +138,7 @@ ve.dm.SurfaceFragment.prototype.adjustRange = function ( start, end ) {
|
|||
}
|
||||
return new ve.dm.SurfaceFragment(
|
||||
this.surface,
|
||||
new ve.Range( this.range.start + ( start || 0 ), this.range.end + ( end || 0 ) ),
|
||||
new ve.Range( this.getRange( true ).start + ( start || 0 ), this.getRange( true ).end + ( end || 0 ) ),
|
||||
this.noAutoSelect
|
||||
);
|
||||
};
|
||||
|
@ -157,7 +157,7 @@ ve.dm.SurfaceFragment.prototype.truncateRange = function ( limit ) {
|
|||
}
|
||||
return new ve.dm.SurfaceFragment(
|
||||
this.surface,
|
||||
this.range.truncate( limit ),
|
||||
this.getRange().truncate( limit ),
|
||||
this.noAutoSelect
|
||||
);
|
||||
};
|
||||
|
@ -174,7 +174,7 @@ ve.dm.SurfaceFragment.prototype.collapseRange = function () {
|
|||
return this;
|
||||
}
|
||||
return new ve.dm.SurfaceFragment(
|
||||
this.surface, new ve.Range( this.range.start ), this.noAutoSelect
|
||||
this.surface, new ve.Range( this.getRange( true ).start ), this.noAutoSelect
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -190,14 +190,14 @@ ve.dm.SurfaceFragment.prototype.trimRange = function () {
|
|||
return this;
|
||||
}
|
||||
// If range is only whitespace
|
||||
if ( this.document.getText( this.range ).trim().length === 0 ) {
|
||||
if ( this.document.getText( this.getRange() ).trim().length === 0 ) {
|
||||
// Collapse range
|
||||
return new ve.dm.SurfaceFragment(
|
||||
this.surface, new ve.Range( this.range.start ), this.noAutoSelect
|
||||
this.surface, new ve.Range( this.getRange( true ).start ), this.noAutoSelect
|
||||
);
|
||||
}
|
||||
return new ve.dm.SurfaceFragment(
|
||||
this.surface, this.document.data.trimOuterSpaceFromRange( this.range ), this.noAutoSelect
|
||||
this.surface, this.document.data.trimOuterSpaceFromRange( this.getRange() ), this.noAutoSelect
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -223,30 +223,30 @@ ve.dm.SurfaceFragment.prototype.expandRange = function ( scope, type ) {
|
|||
var range, node, nodes, parent;
|
||||
switch ( scope || 'parent' ) {
|
||||
case 'word':
|
||||
if ( this.range.getLength() > 0 ) {
|
||||
if ( this.getRange( true ).getLength() > 0 ) {
|
||||
range = ve.Range.newCoveringRange( [
|
||||
this.document.data.getNearestWordRange( this.range.start ),
|
||||
this.document.data.getNearestWordRange( this.range.end )
|
||||
this.document.data.getNearestWordRange( this.getRange( true ).start ),
|
||||
this.document.data.getNearestWordRange( this.getRange( true ).end )
|
||||
] );
|
||||
if ( this.range.isBackwards() ) {
|
||||
if ( this.getRange( true ).isBackwards() ) {
|
||||
range = range.flip();
|
||||
}
|
||||
} else {
|
||||
// optimisation for zero-length ranges
|
||||
range = this.document.data.getNearestWordRange( this.range.start );
|
||||
range = this.document.data.getNearestWordRange( this.getRange( true ).start );
|
||||
}
|
||||
break;
|
||||
case 'annotation':
|
||||
range = this.document.data.getAnnotatedRangeFromSelection( this.range, type );
|
||||
range = this.document.data.getAnnotatedRangeFromSelection( this.getRange(), type );
|
||||
// Adjust selection if it does not contain the annotated range
|
||||
if ( this.range.start > range.start || this.range.end < range.end ) {
|
||||
if ( this.getRange( true ).start > range.start || this.getRange( true ).end < range.end ) {
|
||||
// Maintain range direction
|
||||
if ( this.range.from > this.range.to ) {
|
||||
if ( this.getRange( true ).from > this.getRange( true ).to ) {
|
||||
range = range.flip();
|
||||
}
|
||||
} else {
|
||||
// Otherwise just keep the range as is
|
||||
range = this.range.clone();
|
||||
range = this.getRange();
|
||||
}
|
||||
break;
|
||||
case 'root':
|
||||
|
@ -254,7 +254,7 @@ ve.dm.SurfaceFragment.prototype.expandRange = function ( scope, type ) {
|
|||
break;
|
||||
case 'siblings':
|
||||
// Grow range to cover all siblings
|
||||
nodes = this.document.selectNodes( this.range, 'siblings' );
|
||||
nodes = this.document.selectNodes( this.getRange(), 'siblings' );
|
||||
if ( nodes.length === 1 ) {
|
||||
range = nodes[0].node.getOuterRange();
|
||||
} else {
|
||||
|
@ -266,7 +266,7 @@ ve.dm.SurfaceFragment.prototype.expandRange = function ( scope, type ) {
|
|||
break;
|
||||
case 'closest':
|
||||
// Grow range to cover closest common ancestor node of given type
|
||||
node = this.document.selectNodes( this.range, 'siblings' )[0].node;
|
||||
node = this.document.selectNodes( this.getRange(), 'siblings' )[0].node;
|
||||
parent = node.getParent();
|
||||
while ( parent && parent.getType() !== type ) {
|
||||
node = parent;
|
||||
|
@ -279,7 +279,7 @@ ve.dm.SurfaceFragment.prototype.expandRange = function ( scope, type ) {
|
|||
break;
|
||||
case 'parent':
|
||||
// Grow range to cover the closest common parent node
|
||||
node = this.document.selectNodes( this.range, 'siblings' )[0].node;
|
||||
node = this.document.selectNodes( this.getRange(), 'siblings' )[0].node;
|
||||
parent = node.getParent();
|
||||
if ( !parent ) {
|
||||
return new ve.dm.SurfaceFragment( null );
|
||||
|
@ -314,7 +314,7 @@ ve.dm.SurfaceFragment.prototype.getData = function ( deep ) {
|
|||
if ( !this.surface ) {
|
||||
return [];
|
||||
}
|
||||
return this.document.getData( this.range, deep );
|
||||
return this.document.getData( this.getRange(), deep );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -330,7 +330,7 @@ ve.dm.SurfaceFragment.prototype.getText = function () {
|
|||
}
|
||||
var i, length,
|
||||
text = '',
|
||||
data = this.document.getData( this.range );
|
||||
data = this.document.getData( this.getRange() );
|
||||
for ( i = 0, length = data.length; i < length; i++ ) {
|
||||
if ( data[i].type === undefined ) {
|
||||
// Annotated characters have a string at index 0, plain characters are 1-char strings
|
||||
|
@ -355,8 +355,8 @@ ve.dm.SurfaceFragment.prototype.getAnnotations = function ( all ) {
|
|||
if ( !this.surface ) {
|
||||
return new ve.dm.AnnotationSet( this.getDocument().getStore() );
|
||||
}
|
||||
if ( this.range.getLength() ) {
|
||||
return this.getDocument().data.getAnnotationsFromRange( this.range, all );
|
||||
if ( this.getRange( true ).getLength() ) {
|
||||
return this.getDocument().data.getAnnotationsFromRange( this.getRange(), all );
|
||||
} else {
|
||||
return this.surface.getInsertionAnnotations();
|
||||
}
|
||||
|
@ -375,7 +375,7 @@ ve.dm.SurfaceFragment.prototype.getLeafNodes = function () {
|
|||
if ( !this.surface ) {
|
||||
return [];
|
||||
}
|
||||
return this.document.selectNodes( this.range, 'leaves' );
|
||||
return this.document.selectNodes( this.getRange(), 'leaves' );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -395,7 +395,7 @@ ve.dm.SurfaceFragment.prototype.getCoveredNodes = function () {
|
|||
if ( !this.surface ) {
|
||||
return [];
|
||||
}
|
||||
return this.document.selectNodes( this.range, 'coveredNodes' );
|
||||
return this.document.selectNodes( this.getRange(), 'coveredNodes' );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -413,7 +413,7 @@ ve.dm.SurfaceFragment.prototype.getSiblingNodes = function () {
|
|||
if ( !this.surface ) {
|
||||
return [];
|
||||
}
|
||||
return this.document.selectNodes( this.range, 'siblings' );
|
||||
return this.document.selectNodes( this.getRange(), 'siblings' );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -439,7 +439,7 @@ ve.dm.SurfaceFragment.prototype.select = function () {
|
|||
if ( !this.surface ) {
|
||||
return this;
|
||||
}
|
||||
this.surface.change( null, this.range );
|
||||
this.surface.change( null, this.getRange() );
|
||||
return this;
|
||||
};
|
||||
|
||||
|
@ -469,10 +469,10 @@ ve.dm.SurfaceFragment.prototype.annotateContent = function ( method, nameOrAnnot
|
|||
} else {
|
||||
annotation = ve.dm.annotationFactory.create( nameOrAnnotation, data );
|
||||
}
|
||||
if ( this.range.getLength() ) {
|
||||
if ( this.getRange( true ).getLength() ) {
|
||||
// Apply to selection
|
||||
tx = ve.dm.Transaction.newFromAnnotation( this.document, this.range, method, annotation );
|
||||
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.range ) );
|
||||
tx = ve.dm.Transaction.newFromAnnotation( this.document, this.getRange(), method, annotation );
|
||||
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.getRange() ) );
|
||||
} else {
|
||||
// Apply annotation to stack
|
||||
if ( method === 'set' ) {
|
||||
|
@ -500,7 +500,7 @@ ve.dm.SurfaceFragment.prototype.insertContent = function ( content, annotate ) {
|
|||
return this;
|
||||
}
|
||||
var tx, annotations;
|
||||
if ( this.range.getLength() ) {
|
||||
if ( this.getRange( true ).getLength() ) {
|
||||
this.removeContent();
|
||||
}
|
||||
// Auto-convert content to array of plain text characters
|
||||
|
@ -509,13 +509,13 @@ ve.dm.SurfaceFragment.prototype.insertContent = function ( content, annotate ) {
|
|||
}
|
||||
if ( content.length ) {
|
||||
if ( annotate ) {
|
||||
annotations = this.document.data.getAnnotationsFromOffset( this.range.start - 1 );
|
||||
annotations = this.document.data.getAnnotationsFromOffset( this.getRange( true ).start - 1 );
|
||||
if ( annotations.getLength() > 0 ) {
|
||||
ve.dm.Document.addAnnotationsToData( content, annotations );
|
||||
}
|
||||
}
|
||||
tx = ve.dm.Transaction.newFromInsertion( this.document, this.range.start, content );
|
||||
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.range ) );
|
||||
tx = ve.dm.Transaction.newFromInsertion( this.document, this.getRange( true ).start, content );
|
||||
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.getRange() ) );
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
@ -532,19 +532,18 @@ ve.dm.SurfaceFragment.prototype.removeContent = function () {
|
|||
return this;
|
||||
}
|
||||
var tx;
|
||||
if ( this.range.getLength() ) {
|
||||
tx = ve.dm.Transaction.newFromRemoval( this.document, this.range );
|
||||
// this.range will be translated via the onTransact event handler
|
||||
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.range ) );
|
||||
if ( this.getRange( true ).getLength() ) {
|
||||
tx = ve.dm.Transaction.newFromRemoval( this.document, this.getRange() );
|
||||
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.getRange() ) );
|
||||
// Check if the range didn't get collapsed automatically - this will occur when removing
|
||||
// content across un-mergable nodes because the delete only strips out content leaving
|
||||
// structure at the beginning and end of the range in place
|
||||
if ( this.range.getLength() ) {
|
||||
if ( this.getRange( true ).getLength() ) {
|
||||
// Collapse the range manually
|
||||
this.range = new ve.Range( this.range.start );
|
||||
this.range = new ve.Range( this.getRange( true ).start );
|
||||
if ( !this.noAutoSelect ) {
|
||||
// Update the surface selection
|
||||
this.surface.change( null, this.range );
|
||||
this.surface.change( null, this.getRange() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -565,8 +564,8 @@ ve.dm.SurfaceFragment.prototype.convertNodes = function ( type, attr ) {
|
|||
return this;
|
||||
}
|
||||
var tx =
|
||||
ve.dm.Transaction.newFromContentBranchConversion( this.document, this.range, type, attr );
|
||||
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.range ) );
|
||||
ve.dm.Transaction.newFromContentBranchConversion( this.document, this.getRange(), type, attr );
|
||||
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.getRange() ) );
|
||||
return this;
|
||||
};
|
||||
|
||||
|
@ -597,10 +596,10 @@ ve.dm.SurfaceFragment.prototype.wrapNodes = function ( wrapper ) {
|
|||
if ( !ve.isArray( wrapper ) ) {
|
||||
wrapper = [wrapper];
|
||||
}
|
||||
var tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [], [], [], wrapper ),
|
||||
newRange = new ve.Range( this.range.start, this.range.end + tx.getLengthDifference() );
|
||||
var tx = ve.dm.Transaction.newFromWrap( this.document, this.getRange(), [], [], [], wrapper ),
|
||||
newRange = new ve.Range( this.getRange( true ).start, this.getRange( true ).end + tx.getLengthDifference() );
|
||||
this.surface.change( tx, !this.noAutoSelect && newRange );
|
||||
this.range = newRange;
|
||||
this.setRange( newRange );
|
||||
return this;
|
||||
};
|
||||
|
||||
|
@ -624,22 +623,22 @@ ve.dm.SurfaceFragment.prototype.unwrapNodes = function ( outerDepth, innerDepth
|
|||
}
|
||||
var i, tx, newRange, innerUnwrapper = [], outerUnwrapper = [];
|
||||
|
||||
if ( this.range.end - this.range.start < innerDepth * 2 ) {
|
||||
if ( this.getRange( true ).end - this.getRange( true ).start < innerDepth * 2 ) {
|
||||
throw new Error( 'cannot unwrap by greater depth than maximum theoretical depth of selection' );
|
||||
}
|
||||
|
||||
for ( i = 0; i < innerDepth; i++ ) {
|
||||
innerUnwrapper.push( this.surface.getDocument().data.getData( this.range.start + i ) );
|
||||
innerUnwrapper.push( this.surface.getDocument().data.getData( this.getRange( true ).start + i ) );
|
||||
}
|
||||
for ( i = outerDepth; i > 0; i-- ) {
|
||||
outerUnwrapper.push( this.surface.getDocument().data.getData( this.range.start - i ) );
|
||||
outerUnwrapper.push( this.surface.getDocument().data.getData( this.getRange( true ).start - i ) );
|
||||
}
|
||||
|
||||
tx = ve.dm.Transaction.newFromWrap( this.document, this.range, outerUnwrapper, [], innerUnwrapper, [] );
|
||||
newRange = new ve.Range( this.range.start - outerDepth, this.range.end + outerDepth + tx.getLengthDifference() );
|
||||
tx = ve.dm.Transaction.newFromWrap( this.document, this.getRange(), outerUnwrapper, [], innerUnwrapper, [] );
|
||||
newRange = new ve.Range( this.getRange( true ).start - outerDepth, this.getRange( true ).end + outerDepth + tx.getLengthDifference() );
|
||||
this.surface.change( tx, !this.noAutoSelect && newRange );
|
||||
|
||||
this.range = newRange;
|
||||
this.setRange( newRange );
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -676,19 +675,19 @@ ve.dm.SurfaceFragment.prototype.rewrapNodes = function ( depth, wrapper ) {
|
|||
wrapper = [wrapper];
|
||||
}
|
||||
|
||||
if ( this.range.end - this.range.start < depth * 2 ) {
|
||||
if ( this.getRange( true ).end - this.getRange( true ).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.getData( this.range.start + i ) );
|
||||
unwrapper.push( this.surface.getDocument().data.getData( this.getRange( true ).start + i ) );
|
||||
}
|
||||
|
||||
tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [], [], unwrapper, wrapper );
|
||||
newRange = new ve.Range( this.range.start, this.range.end + tx.getLengthDifference() );
|
||||
tx = ve.dm.Transaction.newFromWrap( this.document, this.getRange(), [], [], unwrapper, wrapper );
|
||||
newRange = new ve.Range( this.getRange( true ).start, this.getRange( true ).end + tx.getLengthDifference() );
|
||||
this.surface.change( tx, !this.noAutoSelect && newRange );
|
||||
|
||||
this.range = newRange;
|
||||
this.setRange( newRange );
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -724,11 +723,11 @@ ve.dm.SurfaceFragment.prototype.wrapAllNodes = function ( wrapper ) {
|
|||
wrapper = [wrapper];
|
||||
}
|
||||
|
||||
tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [], wrapper, [], [] );
|
||||
newRange = new ve.Range( this.range.start, this.range.end + tx.getLengthDifference() );
|
||||
tx = ve.dm.Transaction.newFromWrap( this.document, this.getRange(), [], wrapper, [], [] );
|
||||
newRange = new ve.Range( this.getRange( true ).start, this.getRange( true ).end + tx.getLengthDifference() );
|
||||
this.surface.change( tx, !this.noAutoSelect && newRange );
|
||||
|
||||
this.range = newRange;
|
||||
this.setRange( newRange );
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -757,25 +756,25 @@ ve.dm.SurfaceFragment.prototype.rewrapAllNodes = function ( depth, wrapper ) {
|
|||
return this;
|
||||
}
|
||||
var i, tx, newRange, unwrapper = [],
|
||||
innerRange = new ve.Range( this.range.start + depth, this.range.end - depth );
|
||||
innerRange = new ve.Range( this.getRange( true ).start + depth, this.getRange( true ).end - depth );
|
||||
|
||||
if ( !ve.isArray( wrapper ) ) {
|
||||
wrapper = [wrapper];
|
||||
}
|
||||
|
||||
if ( this.range.end - this.range.start < depth * 2 ) {
|
||||
if ( this.getRange( true ).end - this.getRange( true ).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.getData( this.range.start + i ) );
|
||||
unwrapper.push( this.surface.getDocument().data.getData( this.getRange( true ).start + i ) );
|
||||
}
|
||||
|
||||
tx = ve.dm.Transaction.newFromWrap( this.document, innerRange, unwrapper, wrapper, [], [] );
|
||||
newRange = new ve.Range( this.range.start, this.range.end + tx.getLengthDifference() );
|
||||
newRange = new ve.Range( this.getRange( true ).start, this.getRange( true ).end + tx.getLengthDifference() );
|
||||
this.surface.change( tx, !this.noAutoSelect && newRange );
|
||||
|
||||
this.range = newRange;
|
||||
this.setRange( newRange );
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -807,7 +806,7 @@ ve.dm.SurfaceFragment.prototype.isolateAndUnwrap = function ( isolateForType ) {
|
|||
startSplitNodes = [],
|
||||
endSplitNodes = [],
|
||||
fragment = this,
|
||||
newRange = new ve.Range( this.range.start, this.range.end );
|
||||
newRange = new ve.Range( this.getRange( true ).start, this.getRange( true ).end );
|
||||
|
||||
function createSplits( splitNodes, insertBefore ) {
|
||||
var i, length,
|
||||
|
@ -830,7 +829,7 @@ ve.dm.SurfaceFragment.prototype.isolateAndUnwrap = function ( isolateForType ) {
|
|||
endOffset += endOffsetChange;
|
||||
}
|
||||
|
||||
nodes = this.getDocument().selectNodes( this.range, 'siblings' );
|
||||
nodes = this.getDocument().selectNodes( this.getRange(), 'siblings' );
|
||||
|
||||
// Find start split point, if required
|
||||
startSplitNode = nodes[0].node;
|
||||
|
|
|
@ -9,67 +9,117 @@ QUnit.module( 've.FormatAction' );
|
|||
|
||||
/* Tests */
|
||||
|
||||
function runConverterTest( assert, range, type, attributes, expectedSelection, expectedData, label ) {
|
||||
var dom = ve.createDocumentFromHTML( ve.dm.example.isolationHTML ),
|
||||
function runFormatConverterTest( assert, range, type, attributes, expectedSelection, expectedData, msg ) {
|
||||
var selection,
|
||||
dom = ve.createDocumentFromHTML( ve.dm.example.isolationHTML ),
|
||||
surface = new ve.Surface( new ve.init.Target( $( '<div>' ) ), dom ),
|
||||
formatAction = new ve.FormatAction( surface ),
|
||||
data = ve.copyArray( surface.getModel().getDocument().getFullData() );
|
||||
data = ve.copyArray( surface.getModel().getDocument().getFullData() ),
|
||||
originalData = ve.copyArray( data );
|
||||
|
||||
expectedData( data );
|
||||
|
||||
surface.getModel().change( null, range );
|
||||
formatAction.convert( type, attributes );
|
||||
|
||||
expectedData( data );
|
||||
assert.deepEqual( surface.getModel().getDocument().getFullData(), data, msg + ': data models match' );
|
||||
assert.deepEqual( surface.getModel().getSelection(), expectedSelection, msg + ': selections match' );
|
||||
|
||||
assert.deepEqual( surface.getModel().getDocument().getFullData(), data, label + ': data models match' );
|
||||
assert.deepEqual( surface.getModel().getSelection(), expectedSelection, label + ': selections match' );
|
||||
selection = surface.getModel().undo();
|
||||
|
||||
assert.deepEqual( surface.getModel().getDocument().getFullData(), originalData, msg + ' (undo): data models match' );
|
||||
assert.deepEqual( selection, range, msg + ' (undo): selections match' );
|
||||
|
||||
surface.destroy();
|
||||
}
|
||||
|
||||
QUnit.test( 'convert', 12, function ( assert ) {
|
||||
var rebuilt = { 'changed': { 'rebuilt': 1 } },
|
||||
QUnit.test( 'convert', function ( assert ) {
|
||||
var i,
|
||||
rebuilt = { 'changed': { 'rebuilt': 1 } },
|
||||
created = { 'changed': { 'created': 1 } },
|
||||
createdAndRebuilt = { 'changed': { 'created': 1, 'rebuilt': 1 } };
|
||||
|
||||
runConverterTest( assert, new ve.Range( 14, 16 ), 'MWheading', { level: 2 }, new ve.Range( 14, 16 ), function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 11, 2, { 'type': '/list' }, { 'type': 'MWheading', 'attributes': { 'level': 2 }, 'internal': created } );
|
||||
data.splice( 19, 2, { 'type': '/MWheading' }, { 'type': 'list', 'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt } );
|
||||
}, 'converting partial selection of list item "Item 2" to level 2 MWheading' );
|
||||
|
||||
runConverterTest( assert, new ve.Range( 15, 50 ), 'MWheading', { level: 3 }, new ve.Range( 15, 44 ), function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 11, 2, { 'type': '/list' }, { 'type': 'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
|
||||
data.splice( 19, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
|
||||
data.splice( 27, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
|
||||
data.splice( 38, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
|
||||
data.splice( 46, 2, { 'type': '/MWheading' }, { 'type': 'list', 'attributes': { 'style': 'bullet' }, 'internal': created } );
|
||||
}, 'converting partial selection across two lists surrounding a paragraph' );
|
||||
|
||||
runConverterTest( assert, new ve.Range( 4, 28 ), 'MWheading', { level: 1 }, new ve.Range( 2, 22 ), function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 0, 3, { 'type': 'MWheading', 'attributes': { 'level': 1 }, 'internal': created } );
|
||||
data.splice( 7, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 1 }, 'internal': created } );
|
||||
data.splice( 15, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 1 }, 'internal': created } );
|
||||
data.splice( 23, 3, { 'type': '/MWheading' } );
|
||||
}, 'converting partial selection of all list items to level 1 MWheadings' );
|
||||
|
||||
runConverterTest( assert, new ve.Range( 5, 26 ), 'MWpreformatted', undefined, new ve.Range( 3, 20 ), function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 0, 3, { 'type': 'MWpreformatted', 'internal': created } );
|
||||
data.splice( 7, 4, { 'type': '/MWpreformatted' }, { 'type': 'MWpreformatted', 'internal': created } );
|
||||
data.splice( 15, 4, { 'type': '/MWpreformatted' }, { 'type': 'MWpreformatted', 'internal': created } );
|
||||
data.splice( 23, 3, { 'type': '/MWpreformatted' } );
|
||||
}, 'converting partial selection of some list items to MWpreformatted text' );
|
||||
|
||||
runConverterTest( assert, new ve.Range( 146, 159 ), 'paragraph', undefined, new ve.Range( 146, 159 ), function( data ) {
|
||||
data.splice( 145, 1, { 'type': 'paragraph', 'internal': created } );
|
||||
data.splice( 159, 1, { 'type': '/paragraph' } );
|
||||
}, 'converting heading in list item to paragraph' );
|
||||
|
||||
runConverterTest( assert, new ve.Range( 165, 180 ), 'paragraph', undefined, new ve.Range( 165, 180 ), function( data ) {
|
||||
data.splice( 162, 1, { 'type': 'paragraph', 'internal': created } );
|
||||
data.splice( 183, 1, { 'type': '/paragraph' } );
|
||||
}, 'converting MWpreformatted in list item to paragraph' );
|
||||
createdAndRebuilt = { 'changed': { 'created': 1, 'rebuilt': 1 } },
|
||||
cases = [
|
||||
{
|
||||
'range': new ve.Range( 14, 16 ),
|
||||
'type': 'MWheading',
|
||||
'attributes': { level: 2 },
|
||||
'expectedSelection': new ve.Range( 14, 16 ),
|
||||
'expectedData': function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 11, 2, { 'type': '/list' }, { 'type': 'MWheading', 'attributes': { 'level': 2 }, 'internal': created } );
|
||||
data.splice( 19, 2, { 'type': '/MWheading' }, { 'type': 'list', 'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt } );
|
||||
},
|
||||
'msg': 'converting partial selection of list item "Item 2" to level 2 MWheading'
|
||||
},
|
||||
{
|
||||
'range': new ve.Range( 15, 50 ),
|
||||
'type': 'MWheading',
|
||||
'attributes': { level: 3 },
|
||||
'expectedSelection': new ve.Range( 15, 44 ),
|
||||
'expectedData': function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 11, 2, { 'type': '/list' }, { 'type': 'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
|
||||
data.splice( 19, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
|
||||
data.splice( 27, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
|
||||
data.splice( 38, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
|
||||
data.splice( 46, 2, { 'type': '/MWheading' }, { 'type': 'list', 'attributes': { 'style': 'bullet' }, 'internal': created } );
|
||||
},
|
||||
'msg': 'converting partial selection across two lists surrounding a paragraph'
|
||||
},
|
||||
{
|
||||
'range': new ve.Range( 4, 28 ),
|
||||
'type': 'MWheading',
|
||||
'attributes': { level: 1 },
|
||||
'expectedSelection': new ve.Range( 2, 22 ),
|
||||
'expectedData': function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 0, 3, { 'type': 'MWheading', 'attributes': { 'level': 1 }, 'internal': created } );
|
||||
data.splice( 7, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 1 }, 'internal': created } );
|
||||
data.splice( 15, 4, { 'type': '/MWheading' }, { 'type': 'MWheading', 'attributes': { 'level': 1 }, 'internal': created } );
|
||||
data.splice( 23, 3, { 'type': '/MWheading' } );
|
||||
},
|
||||
'msg': 'converting partial selection of all list items to level 1 MWheadings'
|
||||
},
|
||||
{
|
||||
'range': new ve.Range( 5, 26 ),
|
||||
'type': 'MWpreformatted',
|
||||
'attributes': undefined,
|
||||
'expectedSelection': new ve.Range( 3, 20 ),
|
||||
'expectedData': function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 0, 3, { 'type': 'MWpreformatted', 'internal': created } );
|
||||
data.splice( 7, 4, { 'type': '/MWpreformatted' }, { 'type': 'MWpreformatted', 'internal': created } );
|
||||
data.splice( 15, 4, { 'type': '/MWpreformatted' }, { 'type': 'MWpreformatted', 'internal': created } );
|
||||
data.splice( 23, 3, { 'type': '/MWpreformatted' } );
|
||||
},
|
||||
'msg': 'converting partial selection of some list items to MWpreformatted text'
|
||||
},
|
||||
{
|
||||
'range': new ve.Range( 146, 159 ),
|
||||
'type': 'paragraph',
|
||||
'attributes': undefined,
|
||||
'expectedSelection': new ve.Range( 146, 159 ),
|
||||
'expectedData': function( data ) {
|
||||
data.splice( 145, 1, { 'type': 'paragraph', 'internal': created } );
|
||||
data.splice( 159, 1, { 'type': '/paragraph' } );
|
||||
},
|
||||
'msg': 'converting heading in list item to paragraph'
|
||||
},
|
||||
{
|
||||
'range': new ve.Range( 165, 180 ),
|
||||
'type': 'paragraph',
|
||||
'attributes': undefined,
|
||||
'expectedSelection': new ve.Range( 165, 180 ),
|
||||
'expectedData': function( data ) {
|
||||
data.splice( 162, 1, { 'type': 'paragraph', 'internal': created } );
|
||||
data.splice( 183, 1, { 'type': '/paragraph' } );
|
||||
},
|
||||
'msg': 'converting MWpreformatted in list item to paragraph'
|
||||
}
|
||||
];
|
||||
|
||||
QUnit.expect( cases.length * 4 );
|
||||
for ( i = 0; i < cases.length; i++ ) {
|
||||
runFormatConverterTest( assert, cases[i].range, cases[i].type, cases[i].attributes, cases[i].expectedSelection, cases[i].expectedData, cases[i].msg );
|
||||
}
|
||||
} );
|
||||
|
|
|
@ -9,30 +9,57 @@ QUnit.module( 've.IndentationAction' );
|
|||
|
||||
/* Tests */
|
||||
|
||||
function runIndentationTest( assert, range, method, expectedSelection, expectedData, label ) {
|
||||
var dom = ve.createDocumentFromHTML( ve.dm.example.isolationHTML ),
|
||||
function runIndentationChangeTest( assert, range, method, expectedSelection, expectedData, expectedOriginalData, msg ) {
|
||||
var selection,
|
||||
dom = ve.createDocumentFromHTML( ve.dm.example.isolationHTML ),
|
||||
surface = new ve.Surface( new ve.init.Target( $( '<div>' ) ), dom ),
|
||||
indentationAction = new ve.IndentationAction( surface ),
|
||||
data = ve.copyArray( surface.getModel().getDocument().getFullData() );
|
||||
data = ve.copyArray( surface.getModel().getDocument().getFullData() ),
|
||||
originalData = ve.copyArray( data );
|
||||
|
||||
expectedData( data );
|
||||
if ( expectedOriginalData ) {
|
||||
expectedOriginalData( originalData );
|
||||
}
|
||||
|
||||
surface.getModel().change( null, range );
|
||||
indentationAction[method]();
|
||||
|
||||
expectedData( data );
|
||||
assert.deepEqual( surface.getModel().getDocument().getFullData(), data, msg + ': data models match' );
|
||||
assert.deepEqual( surface.getModel().getSelection(), expectedSelection, msg + ': selections match' );
|
||||
|
||||
assert.deepEqual( surface.getModel().getDocument().getFullData(), data, label + ': data models match' );
|
||||
assert.deepEqual( surface.getModel().getSelection(), expectedSelection, label + ': selections match' );
|
||||
selection = surface.getModel().undo();
|
||||
|
||||
assert.deepEqual( surface.getModel().getDocument().getFullData(), originalData, msg + ' (undo): data models match' );
|
||||
assert.deepEqual( selection, range, msg + ' (undo): selections match' );
|
||||
|
||||
surface.destroy();
|
||||
}
|
||||
|
||||
QUnit.test( 'decrease', 2, function ( assert ) {
|
||||
var rebuilt = { 'changed': { 'rebuilt': 1 } },
|
||||
createdAndRebuilt = { 'changed': { 'created': 2, 'rebuilt': 1 } };
|
||||
var i,
|
||||
rebuilt = { 'changed': { 'rebuilt': 1 } },
|
||||
createdAndRebuilt = { 'changed': { 'created': 2, 'rebuilt': 1 } },
|
||||
cases = [
|
||||
{
|
||||
'range': new ve.Range( 14, 16 ),
|
||||
'method': 'decrease',
|
||||
'expectedSelection': new ve.Range( 14, 16 ),
|
||||
'expectedData': function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 11, 2, { 'type': '/list' }, { 'type': 'paragraph' } );
|
||||
data.splice( 19, 2, { 'type': '/paragraph' }, { 'type': 'list', 'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt } );
|
||||
},
|
||||
'expectedOriginalData': function( data ) {
|
||||
// generated: 'wrapper' is removed by the action and not restored by undo
|
||||
delete data[12].internal;
|
||||
},
|
||||
'msg': 'decrease indentation on partial selection of list item "Item 2"'
|
||||
}
|
||||
];
|
||||
|
||||
runIndentationTest( assert, new ve.Range( 14, 16 ), 'decrease', new ve.Range( 14, 16 ), function( data ) {
|
||||
data[0].internal = rebuilt;
|
||||
data.splice( 11, 2, { 'type': '/list' }, { 'type': 'paragraph' } );
|
||||
data.splice( 19, 2, { 'type': '/paragraph' }, { 'type': 'list', 'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt } );
|
||||
}, 'decrease indentation on partial selection of list item "Item 2"' );
|
||||
QUnit.expect( cases.length * 4 );
|
||||
for ( i = 0; i < cases.length; i++ ) {
|
||||
runIndentationChangeTest( assert, cases[i].range, cases[i].method, cases[i].expectedSelection, cases[i].expectedData, cases[i].expectedOriginalData, cases[i].msg );
|
||||
}
|
||||
} );
|
||||
|
|
|
@ -24,23 +24,26 @@ QUnit.test( 'constructor', 8, function ( assert ) {
|
|||
assert.equal( fragment.getRange().from, 0, 'range is clamped between 0 and document length' );
|
||||
assert.equal( fragment.getRange().to, 61, 'range is clamped between 0 and document length' );
|
||||
assert.strictEqual( fragment.willAutoSelect(), false, 'noAutoSelect values are boolean' );
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'onTransact', 1, function ( assert ) {
|
||||
QUnit.test( 'update', 2, function ( assert ) {
|
||||
var doc = ve.dm.example.createExampleDocument(),
|
||||
surface = new ve.dm.Surface( doc ),
|
||||
fragment1 = new ve.dm.SurfaceFragment( surface, new ve.Range( 1, 56 ) ),
|
||||
fragment2 = new ve.dm.SurfaceFragment( surface, new ve.Range( 2, 4 ) );
|
||||
fragment2 = new ve.dm.SurfaceFragment( surface, new ve.Range( 2, 4 ) ),
|
||||
fragment3 = new ve.dm.SurfaceFragment( surface, new ve.Range( 2, 4 ) );
|
||||
fragment1.removeContent();
|
||||
assert.deepEqual(
|
||||
fragment2.getRange(),
|
||||
new ve.Range( 1, 1 ),
|
||||
'fragment ranges are auto-translated when transactions are processed'
|
||||
'fragment range collapses after removeContent'
|
||||
);
|
||||
surface.undo();
|
||||
assert.deepEqual(
|
||||
fragment3.getRange(),
|
||||
new ve.Range( 4, 4 ),
|
||||
'fragment range moved after undo'
|
||||
);
|
||||
|
||||
fragment1.destroy();
|
||||
fragment2.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'adjustRange', 3, function ( assert ) {
|
||||
|
@ -51,7 +54,6 @@ QUnit.test( 'adjustRange', 3, function ( assert ) {
|
|||
assert.ok( fragment !== adjustedFragment, 'adjustRange produces a new fragment' );
|
||||
assert.deepEqual( fragment.getRange(), new ve.Range( 20, 21 ), 'old fragment is not changed' );
|
||||
assert.deepEqual( adjustedFragment.getRange(), new ve.Range( 1, 56 ), 'new range is used' );
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'collapseRange', 3, function ( assert ) {
|
||||
|
@ -62,8 +64,6 @@ QUnit.test( 'collapseRange', 3, function ( assert ) {
|
|||
assert.ok( fragment !== collapsedFragment, 'collapseRange produces a new fragment' );
|
||||
assert.deepEqual( fragment.getRange(), new ve.Range( 20, 21 ), 'old fragment is not changed' );
|
||||
assert.deepEqual( collapsedFragment.getRange(), new ve.Range( 20, 20 ), 'new range is used' );
|
||||
collapsedFragment.destroy();
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'expandRange (closest)', 1, function ( assert ) {
|
||||
|
@ -76,8 +76,6 @@ QUnit.test( 'expandRange (closest)', 1, function ( assert ) {
|
|||
true,
|
||||
'closest with invalid type results in null fragment'
|
||||
);
|
||||
exapandedFragment.destroy();
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'expandRange (word)', 1, function ( assert ) {
|
||||
|
@ -111,8 +109,6 @@ QUnit.test( 'expandRange (word)', 1, function ( assert ) {
|
|||
word = cases[i].phrase.substring( range.start, range.end );
|
||||
assert.strictEqual( word, cases[i].expected, cases[i].msg + ': text' );
|
||||
assert.strictEqual( cases[i].range.isBackwards(), range.isBackwards(), cases[i].msg + ': range direction' );
|
||||
fragment.destroy();
|
||||
newFragment.destroy();
|
||||
}
|
||||
} );
|
||||
|
||||
|
@ -135,7 +131,6 @@ QUnit.test( 'removeContent', 2, function ( assert ) {
|
|||
new ve.Range( 1, 1 ),
|
||||
'removing content results in a zero-length fragment'
|
||||
);
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'insertContent', 3, function ( assert ) {
|
||||
|
@ -159,7 +154,6 @@ QUnit.test( 'insertContent', 3, function ( assert ) {
|
|||
['3', '2', '1'],
|
||||
'strings get converted into data when inserting content'
|
||||
);
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'wrapNodes/unwrapNodes', 10, function ( assert ) {
|
||||
|
@ -205,7 +199,6 @@ QUnit.test( 'wrapNodes/unwrapNodes', 10, function ( assert ) {
|
|||
fragment.unwrapNodes( 0, 2 );
|
||||
assert.deepEqual( doc.getData(), originalDoc.getData(), 'unwrapping 2 levels restores document to original state' );
|
||||
assert.deepEqual( fragment.getRange(), new ve.Range( 55, 61 ), 'range after unwrapping is same as original range' );
|
||||
fragment.destroy();
|
||||
|
||||
// Make a 1 paragraph into 1 list with 1 item
|
||||
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 9, 12 ) );
|
||||
|
@ -234,13 +227,11 @@ QUnit.test( 'wrapNodes/unwrapNodes', 10, function ( assert ) {
|
|||
fragment.unwrapNodes( 0, 2 );
|
||||
assert.deepEqual( doc.getData(), originalDoc.getData(), 'unwrapping 2 levels restores document to original state' );
|
||||
assert.deepEqual( fragment.getRange(), new ve.Range( 9, 12 ), 'range after unwrapping is same as original range' );
|
||||
fragment.destroy();
|
||||
|
||||
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 8, 34 ) );
|
||||
fragment.unwrapNodes( 3, 1 );
|
||||
assert.deepEqual( fragment.getData(), doc.getData( new ve.Range( 5, 29 ) ), 'unwrapping multiple outer nodes and an inner node' );
|
||||
assert.deepEqual( fragment.getRange(), new ve.Range( 5, 29 ), 'new range contains inner elements' );
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'rewrapNodes', 4, function ( assert ) {
|
||||
|
@ -284,7 +275,6 @@ QUnit.test( 'rewrapNodes', 4, function ( assert ) {
|
|||
// The intermediate stage (plain text attached to the document) would be invalid
|
||||
// if performed as an unwrap and a wrap
|
||||
expectedData = ve.copyArray( doc.getData() );
|
||||
fragment.destroy();
|
||||
|
||||
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 59, 65 ) );
|
||||
fragment.rewrapNodes( 1, [ { 'type': 'heading', 'attributes': { 'level': 1 } } ] );
|
||||
|
@ -296,7 +286,6 @@ QUnit.test( 'rewrapNodes', 4, function ( assert ) {
|
|||
|
||||
assert.deepEqual( doc.getData(), expectedData, 'rewrapping paragraphs as headings' );
|
||||
assert.deepEqual( fragment.getRange(), new ve.Range( 59, 65 ), 'new range contains rewrapping elements' );
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'wrapAllNodes', 10, function ( assert ) {
|
||||
|
@ -335,7 +324,6 @@ QUnit.test( 'wrapAllNodes', 10, function ( assert ) {
|
|||
fragment.unwrapNodes( 0, 2 );
|
||||
assert.deepEqual( doc.getData(), originalDoc.getData(), 'unwrapping 2 levels restores document to original state' );
|
||||
assert.deepEqual( fragment.getRange(), new ve.Range( 55, 61 ), 'range after unwrapping is same as original range' );
|
||||
fragment.destroy();
|
||||
|
||||
// Make a 1 paragraph into 1 list with 1 item
|
||||
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 9, 12 ) );
|
||||
|
@ -364,7 +352,6 @@ QUnit.test( 'wrapAllNodes', 10, function ( assert ) {
|
|||
fragment.unwrapNodes( 0, 2 );
|
||||
assert.deepEqual( doc.getData(), originalDoc.getData(), 'unwrapping 2 levels restores document to original state' );
|
||||
assert.deepEqual( fragment.getRange(), new ve.Range( 9, 12 ), 'range after unwrapping is same as original range' );
|
||||
fragment.destroy();
|
||||
|
||||
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 5, 37 ) );
|
||||
|
||||
|
@ -380,7 +367,6 @@ QUnit.test( 'wrapAllNodes', 10, function ( assert ) {
|
|||
expectedData,
|
||||
'unwrapping 4 levels (table, tableSection, tableRow and tableCell)'
|
||||
);
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
QUnit.test( 'rewrapAllNodes', 6, function ( assert ) {
|
||||
|
@ -410,7 +396,6 @@ QUnit.test( 'rewrapAllNodes', 6, function ( assert ) {
|
|||
'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' );
|
||||
expectedFragment.destroy();
|
||||
|
||||
// Reverse of first test
|
||||
fragment.rewrapAllNodes(
|
||||
|
@ -434,7 +419,6 @@ QUnit.test( 'rewrapAllNodes', 6, function ( assert ) {
|
|||
'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' );
|
||||
fragment.destroy();
|
||||
|
||||
// Rewrap a heading as a paragraph
|
||||
// The intermediate stage (plain text attached to the document) would be invalid
|
||||
|
@ -447,7 +431,6 @@ QUnit.test( 'rewrapAllNodes', 6, function ( assert ) {
|
|||
|
||||
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' );
|
||||
fragment.destroy();
|
||||
} );
|
||||
|
||||
function runIsolateTest( assert, type, range, expected, label ) {
|
||||
|
@ -461,7 +444,6 @@ function runIsolateTest( assert, type, range, expected, label ) {
|
|||
expected( data );
|
||||
|
||||
assert.deepEqual( doc.getFullData(), data, label );
|
||||
fragment.destroy();
|
||||
}
|
||||
|
||||
QUnit.test( 'isolateAndUnwrap', 4, function ( assert ) {
|
||||
|
|
|
@ -2288,7 +2288,7 @@ ve.dm.example.isolationData = [
|
|||
{ 'type': '/tableSection' },
|
||||
{ 'type': '/table' },
|
||||
{ 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
|
||||
'N', 'o', 't', ' ', 'a', 'l', 'l', 'o', 'w', 'e', 'd', ' ', 'b', 'y', ' ', 'd', 'm', ': ',
|
||||
'N', 'o', 't', ' ', 'a', 'l', 'l', 'o', 'w', 'e', 'd', ' ', 'b', 'y', ' ', 'd', 'm', ':',
|
||||
{ 'type': '/paragraph' },
|
||||
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
|
||||
{ 'type': 'listItem' },
|
||||
|
|
|
@ -81,18 +81,15 @@ ve.ui.LinkInspector.prototype.onSetup = function () {
|
|||
if ( fragment.getRange().isCollapsed() ) {
|
||||
// Expand to nearest word
|
||||
expandedFragment = fragment.expandRange( 'word' );
|
||||
fragment.destroy();
|
||||
fragment = expandedFragment;
|
||||
} else {
|
||||
// Trim whitespace
|
||||
trimmedFragment = fragment.trimRange();
|
||||
fragment.destroy();
|
||||
fragment = trimmedFragment;
|
||||
}
|
||||
if ( !fragment.getRange().isCollapsed() ) {
|
||||
// Create annotation from selection
|
||||
truncatedFragment = fragment.truncateRange( 255 );
|
||||
fragment.destroy();
|
||||
fragment = truncatedFragment;
|
||||
annotation = this.getAnnotationFromTarget( fragment.getText() );
|
||||
fragment.annotateContent( 'set', annotation );
|
||||
|
@ -101,12 +98,11 @@ ve.ui.LinkInspector.prototype.onSetup = function () {
|
|||
} else {
|
||||
// Expand range to cover annotation
|
||||
expandedFragment = fragment.expandRange( 'annotation', annotation );
|
||||
fragment.destroy();
|
||||
fragment = expandedFragment;
|
||||
}
|
||||
|
||||
// Update selection
|
||||
fragment.select().destroy();
|
||||
fragment.select();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -128,8 +124,6 @@ ve.ui.LinkInspector.prototype.onOpen = function () {
|
|||
this.targetInput.setAnnotation( annotation );
|
||||
this.targetInput.$input.focus().select();
|
||||
}, this ), 200 );
|
||||
|
||||
fragment.destroy();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -174,7 +168,6 @@ ve.ui.LinkInspector.prototype.onClose = function ( action ) {
|
|||
// Insert default text and select it
|
||||
fragment.insertContent( target, false );
|
||||
adjustedFragment = fragment.adjustRange( -target.length, 0 );
|
||||
fragment.destroy();
|
||||
fragment = adjustedFragment;
|
||||
|
||||
// Move cursor to the end of the inserted content
|
||||
|
@ -204,8 +197,6 @@ ve.ui.LinkInspector.prototype.onClose = function ( action ) {
|
|||
);
|
||||
// Reset state
|
||||
this.isNewAnnotation = false;
|
||||
|
||||
fragment.destroy();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -238,8 +238,6 @@ ve.ui.Context.prototype.update = function () {
|
|||
// Remember selection for next time
|
||||
this.selection = selection.clone();
|
||||
|
||||
fragment.destroy();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
|
|
|
@ -79,7 +79,6 @@ ve.ui.Toolbar.prototype.onContextChange = function () {
|
|||
}
|
||||
}
|
||||
this.emit( 'updateState', nodes, fragment.getAnnotations(), fragment.getAnnotations( true ) );
|
||||
fragment.destroy();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -170,7 +170,7 @@ ve.Surface.prototype.isEnabled = function () {
|
|||
* @method
|
||||
*/
|
||||
ve.Surface.prototype.resetSelection = function () {
|
||||
this.model.getFragment().select().destroy();
|
||||
this.model.getFragment().select();
|
||||
this.view.surfaceObserver.poll();
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue