mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-24 06:24:08 +00:00
Simplified transaction model, introduced isPartial for some deletes
This commit is contained in:
parent
e3fc95f41a
commit
dbe4890ed6
|
@ -12,26 +12,22 @@ es.SurfaceModel = function( doc ) {
|
|||
|
||||
// Properties
|
||||
this.doc = doc;
|
||||
this.selection = new es.Range();
|
||||
this.states = [[]];
|
||||
this.initializeState( this.states.length - 1 );
|
||||
this.selection = null;
|
||||
this.history = [];
|
||||
this.historyIndex = 0;
|
||||
this.currentLengthDifference = 0;
|
||||
|
||||
// TODO magic number move to configuration
|
||||
// Configuration
|
||||
this.distanceLimit = 24;
|
||||
this.lengthDifferenceLimit = 24;
|
||||
|
||||
// DEBUG don't commit
|
||||
var _this = this;
|
||||
this.addListener( 'transact', function() { console.log( _this.history ); } );
|
||||
};
|
||||
|
||||
/* Methods */
|
||||
|
||||
es.SurfaceModel.prototype.initializeState = function( stateIndex ) {
|
||||
if ( this.states[stateIndex] === undefined ) {
|
||||
throw 'Invalid state index error. State index our of range: ' + stateIndex;
|
||||
}
|
||||
this.currentStateIndex = stateIndex;
|
||||
this.currentState = this.states[stateIndex];
|
||||
this.currentStateDistance = 0;
|
||||
this.currentStateLengthDifference = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the document model of the surface.
|
||||
|
@ -44,10 +40,10 @@ es.SurfaceModel.prototype.getDocument = function() {
|
|||
};
|
||||
|
||||
/**
|
||||
* Gets the selection for the current state.
|
||||
* Gets the selection
|
||||
*
|
||||
* @method
|
||||
* @returns {es.Range} Current state's selection
|
||||
* @returns {es.Range} Current selection
|
||||
*/
|
||||
es.SurfaceModel.prototype.getSelection = function() {
|
||||
return this.selection;
|
||||
|
@ -57,34 +53,37 @@ es.SurfaceModel.prototype.getSelection = function() {
|
|||
* Changes the selection.
|
||||
*
|
||||
* If changing the selection at a high frequency (such as while dragging) use the combine argument
|
||||
* to avoid them being split up into multiple states.
|
||||
* to avoid them being split up into multiple history items
|
||||
*
|
||||
* @method
|
||||
* @param {es.Range} selection
|
||||
* @param {Boolean} combine Whether to prevent this transaction from causing a state push
|
||||
* @param {Boolean} combine Whether to prevent this transaction from causing a history push
|
||||
*/
|
||||
es.SurfaceModel.prototype.select = function( selection, combine ) {
|
||||
es.SurfaceModel.prototype.select = function( selection, isManual ) {
|
||||
selection.normalize();
|
||||
if ( !combine && this.shouldPushState( selection ) ) {
|
||||
this.pushState();
|
||||
|
||||
this.selection = selection;
|
||||
|
||||
if ( isManual ) {
|
||||
// check if the last thing is a selection, if so, swap it.
|
||||
this.pushSelection( selection );
|
||||
}
|
||||
// Filter out calls to select if they do not change the selection values
|
||||
var selectionChanged = !this.selection || (
|
||||
this.selection.from !== selection.from ||
|
||||
this.selection.to !== selection.to
|
||||
);
|
||||
if ( selectionChanged ) {
|
||||
var lastAction = this.states[this.states.length - 1];
|
||||
if ( lastAction instanceof es.Range ) {
|
||||
this.currentStateDistance += Math.abs(
|
||||
selection.from - this.states[this.states.length - 1].from
|
||||
);
|
||||
}
|
||||
this.currentState.push( selection );
|
||||
this.selection = selection;
|
||||
if ( selectionChanged ) {
|
||||
this.emit( 'select', this.selection.clone() );
|
||||
}
|
||||
|
||||
this.emit( 'select', this.selection.clone() );
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a selection (which is really just a marker for when we stop undo/redo) to the history.
|
||||
* For the history, selections are just markers, so we don't want to record many of them in a row.
|
||||
*
|
||||
* @method
|
||||
* @param {es.Range} selection
|
||||
*/
|
||||
es.SurfaceModel.prototype.pushSelection = function( selection ) {
|
||||
if ( this.history[ this.history.length - 1 ] instanceof es.Range ) {
|
||||
this.history[ this.history.length - 1 ] = selection;
|
||||
} else {
|
||||
this.history.push( selection );
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -92,53 +91,81 @@ es.SurfaceModel.prototype.select = function( selection, combine ) {
|
|||
* Applies a series of transactions to the content data.
|
||||
*
|
||||
* If committing multiple transactions which are the result of a single user action and need to be
|
||||
* part of a single state, use the combine argument for all but the last one to avoid them being
|
||||
* split up into multple states.
|
||||
* part of a single history item, use the isPartial argument for all but the last one to avoid them being
|
||||
* split up into multple history items.
|
||||
*
|
||||
* @method
|
||||
* @param {es.TransactionModel} transactions Tranasction to apply to the document
|
||||
* @param {Boolean} combine Whether to prevent this transaction from causing a state push
|
||||
* @param {boolean} isPartial whether this transaction is part of a larger logical grouping of transactions
|
||||
* (such as when replacing - delete, then insert)
|
||||
*/
|
||||
es.SurfaceModel.prototype.transact = function( transaction, combine ) {
|
||||
if ( !combine && this.shouldPushState( transaction ) ) {
|
||||
this.pushState();
|
||||
}
|
||||
this.currentStateLengthDifference += transaction.getLengthDifference();
|
||||
es.SurfaceModel.prototype.transact = function( transaction, isPartial ) {
|
||||
console.log( 'tx:' + $.map( transaction.getOperations(), function(tx) { return tx.type; } ).join(",")
|
||||
+ ' isPartial:' + isPartial );
|
||||
this.doc.commit( transaction );
|
||||
this.currentState.push( transaction );
|
||||
|
||||
// if we have changed the kind of operation (delete -> insert or insert -> delete or annotations )
|
||||
// then push a new selection onto the history, to mark where the undo/redo should end.
|
||||
var d = transaction.getLengthDifference();
|
||||
if (
|
||||
!isPartial &&
|
||||
(
|
||||
( d === 0 ) ||
|
||||
( this.currentLengthDifference < 0 && d > 0 ) ||
|
||||
( this.currentLengthDifference > 0 && d < 0 ) ||
|
||||
( Math.abs( this.currentLengthDifference ) > this.lengthDifferenceLimit )
|
||||
)
|
||||
) {
|
||||
this.currentLengthDifference = d;
|
||||
this.history.push( this.selection );
|
||||
} else {
|
||||
this.currentLengthDifference += d;
|
||||
}
|
||||
|
||||
this.history.push( transaction );
|
||||
|
||||
this.emit( 'transact', transaction );
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reverses one or more states.
|
||||
* We assume the user wants to undo some visible change to the document, so we only count
|
||||
* states that contain at least one transaction that changed the document somehow.
|
||||
* Reverses one or more history items.
|
||||
*
|
||||
* @method
|
||||
* @param {Integer} Number of document-changing states to reverse
|
||||
* @param {Integer} Number of history items to roll back
|
||||
*/
|
||||
es.SurfaceModel.prototype.undo = function( transactionsToUndo ) {
|
||||
es.SurfaceModel.prototype.undo = function( statesToUndo ) {
|
||||
|
||||
console.log( 'about to undo...' );
|
||||
console.log( this.states );
|
||||
console.log( 'currentState: ' + this.currentState );
|
||||
console.log( 'currentStateIndex: ' + this.currentStateIndex );
|
||||
|
||||
while ( transactionsToUndo ) {
|
||||
var hadTransaction = false;
|
||||
lengthDifference = 0;
|
||||
|
||||
var state = this.currentState;
|
||||
while ( statesToUndo ) {
|
||||
statesToUndo--;
|
||||
|
||||
var i = state.length - 1;
|
||||
while ( i-- ) {
|
||||
if ( state[i] instanceof es.TransactionModel ) {
|
||||
hadTransaction = true;
|
||||
this.doc.rollback( state[i] );
|
||||
if ( this.currentState.length ) {
|
||||
for (var i = this.currentState.length - 1; i >= 0; i-- ) {
|
||||
lengthDifference += this.currentState[i].getLengthDifference();
|
||||
this.doc.rollback( this.currentState[i] );
|
||||
}
|
||||
this.emit( 'undo', this.currentState );
|
||||
}
|
||||
this.emit( 'undo', state );
|
||||
|
||||
if ( hadTransaction ) {
|
||||
transactionsToUndo--;
|
||||
}
|
||||
// do we also want all the effects of initializeState? currentStateDistance to be 0, currentStateLengthDifference?
|
||||
this.initializeState( this.currentStateIndex - 1 );
|
||||
if ( this.currentStateIndex > 0 ) {
|
||||
this.initializeState( this.currentStateIndex - 1 );
|
||||
}
|
||||
}
|
||||
|
||||
console.log( 'after undo...' );
|
||||
console.log( this.states );
|
||||
console.log( 'currentState: ' + this.currentState );
|
||||
console.log( 'currentStateIndex: ' + this.currentStateIndex );
|
||||
|
||||
// TODO - make the appropriate selection now
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -152,107 +179,6 @@ es.SurfaceModel.prototype.redo = function( steps ) {
|
|||
this.emit( 'redo'/*, transaction/selection*/ );
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if it's an appropriate time to push the state.
|
||||
*
|
||||
* @method
|
||||
* @returns {Boolean} Whether the state should be pushed
|
||||
*/
|
||||
es.SurfaceModel.prototype.shouldPushState = function( nextAction ) {
|
||||
// Never push a new state if the current one is empty
|
||||
if ( !this.currentState.length ) {
|
||||
return false;
|
||||
}
|
||||
var lastAction = this.currentState[this.currentState.length - 1],
|
||||
nextDirection,
|
||||
lastDirection;
|
||||
if (
|
||||
// Check that types match
|
||||
nextAction instanceof es.Range && lastAction instanceof es.Range
|
||||
) {
|
||||
if (
|
||||
// 2 or more select actions in a row are required to detect a direction
|
||||
this.states.length >= 2 && this.states[this.states.length - 2] instanceof es.Range
|
||||
) {
|
||||
// Check we haven't changed directions
|
||||
lastDirection = this.states[this.states.length - 2].from - lastAction.from;
|
||||
nextDirection = lastAction.from - nextAction.from;
|
||||
if (
|
||||
// Both movements are in the same direction
|
||||
( lastDirection < 0 && nextDirection < 0 ) ||
|
||||
( lastDirection > 0 && nextDirection > 0 )
|
||||
) {
|
||||
// Check we are still within the distance threshold
|
||||
if (
|
||||
Math.abs( nextAction.from - lastAction.from ) + this.currentStateDistance <
|
||||
this.distanceLimit
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
// Check that types match
|
||||
nextAction instanceof es.TransactionModel && lastAction instanceof es.TransactionModel
|
||||
) {
|
||||
// Check if we've changed directions (insert vs remove)
|
||||
lastLengthDifference = lastAction.getLengthDifference();
|
||||
nextLengthDifference = nextAction.getLengthDifference();
|
||||
if (
|
||||
// Both movements are in the same direction
|
||||
( lastLengthDifference < 0 && nextLengthDifference < 0 ) ||
|
||||
( lastLengthDifference > 0 && nextLengthDifference > 0 )
|
||||
) {
|
||||
// Check we are still within the length difference threshold
|
||||
if (
|
||||
nextLengthDifference + this.currentStateLengthDifference <
|
||||
this.lengthDifferenceLimit
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes any undone states and pushes a new state to the stack.
|
||||
*
|
||||
* @method
|
||||
*/
|
||||
es.SurfaceModel.prototype.pushState = function() {
|
||||
// Automatically drop undone states - we are now moving in a new direction
|
||||
if ( this.states[this.states.length - 1] !== this.currentState ) {
|
||||
for ( var i = this.states.length - 1; i > this.currentStateIndex; i-- ) {
|
||||
this.emit( 'popState', this.states.pop() );
|
||||
}
|
||||
}
|
||||
// Push a new state to the stack
|
||||
this.optimizeState( this.states.length - 1 );
|
||||
this.states.push( [] );
|
||||
this.initializeState( this.states.length - 1 );
|
||||
this.emit( 'pushState' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove irrelevant selection actions from a given state
|
||||
* TODO: replace this with code to remove irrelevant selections as they are pushed
|
||||
* (for instance, between two insertions).
|
||||
* @param {Integer}
|
||||
*/
|
||||
es.SurfaceModel.prototype.optimizeState = function( stateIndex ) {
|
||||
var skipSelects = false,
|
||||
newState = [];
|
||||
for ( var i = this.states[stateIndex].length - 1; i >= 0; i-- ) {
|
||||
var action = this.states[stateIndex][i];
|
||||
if ( !( action instanceof es.Range && skipSelects ) ) {
|
||||
newState.push( action );
|
||||
skipSelects = true;
|
||||
}
|
||||
}
|
||||
this.states[stateIndex] = newState;
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
es.extendClass( es.SurfaceModel, es.EventEmitter );
|
||||
|
|
|
@ -321,7 +321,7 @@ es.SurfaceView.prototype.onMouseDown = function( e ) {
|
|||
// Reset the initial left position
|
||||
this.cursor.initialLeft = null;
|
||||
// Apply new selection
|
||||
this.model.select( selection );
|
||||
this.model.select( selection, true );
|
||||
}
|
||||
// If the inut isn't already focused, focus it and select it's contents
|
||||
if ( !this.$input.is( ':focus' ) ) {
|
||||
|
@ -381,7 +381,7 @@ es.SurfaceView.prototype.onMouseMove = function( e ) {
|
|||
es.SurfaceView.prototype.onMouseUp = function( e ) {
|
||||
if ( e.which === 1 ) { // left mouse button
|
||||
this.mouse.selectingMode = this.mouse.selectedRange = null;
|
||||
this.model.select( this.currentSelection );
|
||||
this.model.select( this.currentSelection, true );
|
||||
// We have to manually call this because the selection will not have changed between the
|
||||
// most recent mousemove and this mouseup
|
||||
this.contextView.set();
|
||||
|
@ -544,7 +544,7 @@ es.SurfaceView.prototype.onKeyUp = function( e ) {
|
|||
return true;
|
||||
};
|
||||
|
||||
es.SurfaceView.prototype.handleDelete = function( backspace ) {
|
||||
es.SurfaceView.prototype.handleDelete = function( backspace, isPartial ) {
|
||||
var selection = this.currentSelection.clone(),
|
||||
sourceOffset,
|
||||
targetOffset,
|
||||
|
@ -575,7 +575,7 @@ es.SurfaceView.prototype.handleDelete = function( backspace ) {
|
|||
}
|
||||
|
||||
selection.from = selection.to = targetOffset;
|
||||
this.model.select( selection, true );
|
||||
this.model.select( selection );
|
||||
|
||||
if ( sourceNode === targetNode ||
|
||||
( typeof sourceSplitableNode !== 'undefined' &&
|
||||
|
@ -583,12 +583,12 @@ es.SurfaceView.prototype.handleDelete = function( backspace ) {
|
|||
tx = this.model.getDocument().prepareRemoval(
|
||||
new es.Range( targetOffset, sourceOffset )
|
||||
);
|
||||
this.model.transact( tx, true );
|
||||
this.model.transact( tx, isPartial );
|
||||
} else {
|
||||
tx = this.model.getDocument().prepareInsertion(
|
||||
targetOffset, sourceNode.model.getContentData()
|
||||
);
|
||||
this.model.transact( tx, true );
|
||||
this.model.transact( tx, isPartial );
|
||||
|
||||
var nodeToDelete = sourceNode;
|
||||
es.DocumentNode.traverseUpstream( nodeToDelete, function( node ) {
|
||||
|
@ -603,14 +603,14 @@ es.SurfaceView.prototype.handleDelete = function( backspace ) {
|
|||
range.from = this.documentView.getOffsetFromNode( nodeToDelete, false );
|
||||
range.to = range.from + nodeToDelete.getElementLength();
|
||||
tx = this.model.getDocument().prepareRemoval( range );
|
||||
this.model.transact( tx, true );
|
||||
this.model.transact( tx, isPartial );
|
||||
}
|
||||
} else {
|
||||
// selection removal
|
||||
tx = this.model.getDocument().prepareRemoval( selection );
|
||||
this.model.transact( tx, true );
|
||||
this.model.transact( tx, isPartial );
|
||||
selection.from = selection.to = selection.start;
|
||||
this.model.select( selection, true );
|
||||
this.model.select( selection );
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -618,7 +618,7 @@ es.SurfaceView.prototype.handleEnter = function() {
|
|||
var selection = this.currentSelection.clone(),
|
||||
tx;
|
||||
if ( selection.from !== selection.to ) {
|
||||
this.handleDelete();
|
||||
this.handleDelete( false, true );
|
||||
}
|
||||
var node = this.documentView.getNodeFromOffset( selection.to, false ),
|
||||
nodeOffset = this.documentView.getOffsetFromNode( node, false );
|
||||
|
@ -631,7 +631,7 @@ es.SurfaceView.prototype.handleEnter = function() {
|
|||
nodeOffset + node.getElementLength(),
|
||||
[ { 'type': 'paragraph' }, { 'type': '/paragraph' } ]
|
||||
);
|
||||
this.model.transact( tx, true );
|
||||
this.model.transact( tx );
|
||||
selection.from = selection.to = nodeOffset + node.getElementLength() + 1;
|
||||
} else {
|
||||
var stack = [],
|
||||
|
@ -658,11 +658,11 @@ es.SurfaceView.prototype.handleEnter = function() {
|
|||
return true;
|
||||
} );
|
||||
tx = this.documentView.model.prepareInsertion( selection.to, stack );
|
||||
this.model.transact( tx, true );
|
||||
this.model.transact( tx );
|
||||
selection.from = selection.to =
|
||||
this.model.getDocument().getRelativeContentOffset( selection.to, 1 );
|
||||
}
|
||||
this.model.select( selection, true );
|
||||
this.model.select( selection );
|
||||
};
|
||||
|
||||
es.SurfaceView.prototype.insertFromInput = function() {
|
||||
|
@ -697,12 +697,12 @@ es.SurfaceView.prototype.insertFromInput = function() {
|
|||
var data = val.split('');
|
||||
es.DocumentModel.addAnnotationsToData( data, this.getInsertionAnnotations() );
|
||||
tx = this.model.getDocument().prepareInsertion( selection.from, data );
|
||||
this.model.transact( tx, true );
|
||||
this.model.transact( tx );
|
||||
|
||||
// Move the selection
|
||||
selection.from += val.length;
|
||||
selection.to += val.length;
|
||||
this.model.select( selection, true );
|
||||
this.model.select( selection );
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -832,7 +832,7 @@ es.SurfaceView.prototype.moveCursor = function( direction, unit ) {
|
|||
} else {
|
||||
selection.from = selection.to = to;
|
||||
}
|
||||
this.model.select( selection );
|
||||
this.model.select( selection, true );
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue