2011-11-02 21:00:55 +00:00
|
|
|
/**
|
|
|
|
* Creates an es.SurfaceModel object.
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @constructor
|
2011-11-22 22:59:05 +00:00
|
|
|
* @extends {es.EventEmitter}
|
2011-11-02 21:00:55 +00:00
|
|
|
* @param {es.DocumentModel} doc Document model to create surface for
|
|
|
|
*/
|
|
|
|
es.SurfaceModel = function( doc ) {
|
2011-11-22 22:59:05 +00:00
|
|
|
// Inheritance
|
|
|
|
es.EventEmitter.call( this );
|
|
|
|
|
|
|
|
// Properties
|
2011-11-02 21:00:55 +00:00
|
|
|
this.doc = doc;
|
2011-11-22 22:59:05 +00:00
|
|
|
this.selection = new es.Range();
|
|
|
|
this.states = [[]];
|
|
|
|
this.initializeState( this.states.length - 1 );
|
|
|
|
|
|
|
|
// Configuration
|
|
|
|
this.distanceLimit = 24;
|
|
|
|
this.lengthDifferenceLimit = 24;
|
2011-11-02 21:00:55 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/* Methods */
|
|
|
|
|
2011-11-22 22:59:05 +00:00
|
|
|
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.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {es.DocumentModel} Document model of the surface
|
|
|
|
*/
|
2011-11-02 21:00:55 +00:00
|
|
|
es.SurfaceModel.prototype.getDocument = function() {
|
|
|
|
return this.doc;
|
|
|
|
};
|
2011-11-22 22:59:05 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the selection for the current state.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {es.Range} Current state's selection
|
|
|
|
*/
|
|
|
|
es.SurfaceModel.prototype.getSelection = function() {
|
|
|
|
return this.selection;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {es.Range} selection
|
|
|
|
* @param {Boolean} combine Whether to prevent this transaction from causing a state push
|
|
|
|
*/
|
|
|
|
es.SurfaceModel.prototype.select = function( selection, combine ) {
|
2011-12-01 01:01:27 +00:00
|
|
|
selection.normalize();
|
2011-11-22 22:59:05 +00:00
|
|
|
if ( !combine && this.shouldPushState( selection ) ) {
|
|
|
|
this.pushState();
|
|
|
|
}
|
2011-11-23 00:36:46 +00:00
|
|
|
// Filter out calls to select if they do not change the selection values
|
2011-11-30 23:54:12 +00:00
|
|
|
var selectionChanged = !this.selection || (
|
|
|
|
this.selection.from !== selection.from ||
|
|
|
|
this.selection.to !== selection.to
|
2011-11-30 23:50:32 +00:00
|
|
|
);
|
2011-12-01 01:03:34 +00:00
|
|
|
if ( selectionChanged ) {
|
2011-11-23 00:36:46 +00:00
|
|
|
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 );
|
2011-11-30 23:54:12 +00:00
|
|
|
this.selection = selection;
|
2011-11-30 23:50:32 +00:00
|
|
|
if ( selectionChanged ) {
|
|
|
|
this.emit( 'select', this.selection.clone() );
|
|
|
|
}
|
2011-11-23 00:36:46 +00:00
|
|
|
}
|
2011-11-22 22:59:05 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {es.TransactionModel} transactions Tranasction to apply to the document
|
|
|
|
* @param {Boolean} combine Whether to prevent this transaction from causing a state push
|
|
|
|
*/
|
|
|
|
es.SurfaceModel.prototype.transact = function( transaction, combine ) {
|
|
|
|
if ( !combine && this.shouldPushState( transaction ) ) {
|
|
|
|
this.pushState();
|
|
|
|
}
|
|
|
|
this.currentStateLengthDifference += transaction.getLengthDifference();
|
|
|
|
this.doc.commit( transaction );
|
|
|
|
this.currentState.push( transaction );
|
|
|
|
this.emit( 'transact', transaction );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2011-12-01 19:07:40 +00:00
|
|
|
* 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.
|
2011-11-22 22:59:05 +00:00
|
|
|
*
|
|
|
|
* @method
|
2011-12-01 19:07:40 +00:00
|
|
|
* @param {Integer} Number of document-changing states to reverse
|
2011-11-22 22:59:05 +00:00
|
|
|
*/
|
2011-12-01 19:07:40 +00:00
|
|
|
es.SurfaceModel.prototype.undo = function( transactionsToUndo ) {
|
|
|
|
/**
|
|
|
|
* Undo a state.
|
|
|
|
* @return {Boolean} whether visible change was undone.
|
|
|
|
*/
|
|
|
|
function undoState( state ) {
|
2011-12-01 19:08:18 +00:00
|
|
|
var hadTransaction = false;
|
2011-12-01 19:07:40 +00:00
|
|
|
var i = state.length - 1;
|
|
|
|
while ( i-- ) {
|
|
|
|
if ( state[i] instanceof es.TransactionModel ) {
|
2011-12-01 19:08:18 +00:00
|
|
|
hadTransaction = true;
|
2011-12-01 19:07:40 +00:00
|
|
|
this.doc.rollback( state[i] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.emit( 'undo', state );
|
2011-12-01 19:08:18 +00:00
|
|
|
return hadTransaction;
|
2011-12-01 19:07:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
while ( transactionsToUndo ) {
|
|
|
|
var hadTransaction = undoState( this.currentState );
|
|
|
|
if ( hadTransaction ) {
|
|
|
|
transactionsToUndo--;
|
|
|
|
}
|
|
|
|
// do we also want all the effects of initializeState? currentStateDistance to be 0, currentStateLengthDifference?
|
|
|
|
this.initializeState( this.currentStateIndex - 1 );
|
|
|
|
}
|
2011-11-22 22:59:05 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Repeats one or more selections and transactions.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {Integer} steps Number of steps to repeat
|
|
|
|
*/
|
2011-11-30 22:06:19 +00:00
|
|
|
es.SurfaceModel.prototype.redo = function( steps ) {
|
2011-11-22 22:59:05 +00:00
|
|
|
// TODO: Implement me!
|
|
|
|
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
|
2011-11-23 00:36:46 +00:00
|
|
|
this.optimizeState( this.states.length - 1 );
|
2011-11-22 22:59:05 +00:00
|
|
|
this.states.push( [] );
|
|
|
|
this.initializeState( this.states.length - 1 );
|
|
|
|
this.emit( 'pushState' );
|
|
|
|
};
|
|
|
|
|
2011-12-01 19:07:40 +00:00
|
|
|
/**
|
|
|
|
* 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}
|
|
|
|
*/
|
2011-11-23 00:36:46 +00:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2011-11-22 22:59:05 +00:00
|
|
|
/* Inheritance */
|
|
|
|
|
|
|
|
es.extendClass( es.SurfaceModel, es.EventEmitter );
|