mediawiki-extensions-Visual.../modules/es/models/es.HistoryModel.js
Trevor Parscal 631323b9bd * Refactored es.HistoryModel to always be working from a single array rather than a buffer and an array
* Added support for associating a selection with a state
2011-11-21 23:51:37 +00:00

191 lines
5.6 KiB
JavaScript

/**
* Creates an es.HistoryModel object.
*
* @class
* @constructor
* @extends {es.EventEmitter}
* @param {es.DocumentModel} doc Document being tracked and modified
*/
es.HistoryModel = function( doc ) {
// Inheritance
es.EventEmitter.call( this );
// Properties
this.doc = doc;
this.currentState = new es.HistoryStateModel();
this.currentStateIndex = 0;
this.states = [this.currentState];
this.currentStateDiff = 0;
// Configuration
this.maxStateDiff = 24;
};
/* Methods */
/**
* Gets the index of the current state.
*
*
*/
es.HistoryModel.prototype.getCurrentStateIndex = function() {
return this.currentStateIndex;
};
/**
* Gets the index of the current state.
*
*
*/
es.HistoryModel.prototype.getCurrentStateSelection = function() {
return this.currentState.getSelection();
};
/**
* Gets the number of states available.
*
* @method
*/
es.HistoryModel.prototype.getStateCount = function() {
return this.states.length;
};
/**
* Gets a copy of the list of states.
*
* @method
* @param {Boolean} deep Whether to make a deep copy (can be slow)
* @returns {Array[]} List of states, each a list of transactions
*/
es.HistoryModel.prototype.getStates = function( deep ) {
return deep ? es.copyArray( this.states ) : this.states.slice( 0 );
};
/**
* Gets a copy of the list of transactions, which are not yet part of a state.
*
* @method
* @param {Boolean} deep Whether to make a deep copy (can be slow)
* @returns {es.TransactionModel[]} List of transactions
*/
es.HistoryModel.prototype.getTransactions = function( deep ) {
return deep ? es.copyArray( this.transactions ) : this.transactions.slice( 0 );
};
/**
* Commits a transaction.
*
* Unless the accumulate option is used a state will be automatically pushed before committing
* if the transaction is of a different type as the previous one in the transaction buffer, or if
* the transaction would produce a content length difference beyond the configured maximum.
*
* @method
* @param {es.TransactionModel} transaction Transaction to commit
* @param {es.Range} [selection] Selection to use after the transaction has been applied
* @param {Boolean} [accumulate] Prevent automatic state pushing
*/
es.HistoryModel.prototype.commit = function( transaction, selection, accumulate ) {
var transactionDiff = Math.abs( transaction.getLengthDiff() );
// Unless we should intentionally accumulate transactions or this is the first one for this
// state, automatically push state
var transactionCount = this.currentState.getTransactionCount();
if ( !accumulate && transactionCount ) {
if (
// If the transactions are of a different type
this.currentState.getTransactions()[transactionCount - 1].type !== transaction.type ||
// This transaction would make the state longer than the maximum length
this.currentStateDiff + transactionDiff > this.maxStateDiff
) {
this.pushState();
}
}
this.currentState.pushTransaction( transaction );
this.currentStateDiff += transactionDiff;
// Apply transaction to the document
this.doc.commit( transaction );
// Emit a do event with the transaction that was just committed
this.emit( 'do', transaction );
};
/**
* Moves transactions in the buffer into a new state.
*
* @method
*/
es.HistoryModel.prototype.pushState = function() {
// If any transactions have been pushed since the last state push
if ( this.currentState.getTransactionCount() ) {
// If the current state is not the most recently added state
if ( this.currentStateIndex < this.states.length - 1 ) {
// Forget about states newer than the current one
this.states.splice(
this.currentStateIndex, this.states.length - this.currentStateIndex
);
}
// Create a new current state
this.currentState = new es.HistoryStateModel();
// Add the new current state to the stack
this.states.push( this.currentState );
// Reset the state diff counter
this.currentStateDiff = 0;
// Move the current state index to the end (should be equivilant of ++)
this.currentStateIndex = this.states.length - 1;
}
// Emit the completed
this.emit( 'pushState', this.states[this.states.length - 1] );
};
/**
*
*
* @method
*/
es.HistoryModel.prototype.undo = function( steps ) {
if ( steps === undefined ) {
steps = 1;
}
// Stop undo just before the first state
var previousStateIndex = this.currentStateIndex;
this.currentStateIndex = Math.max( -1, this.currentStateIndex - steps );
if ( previousStateIndex > this.currentStateIndex ) {
for ( var i = previousStateIndex; i > this.currentStateIndex; i-- ) {
// Apply transaction to the document
var transactions = this.states[i].getTransactions();
for ( var j = transactions.length - 1; j >= 0; j-- ) {
this.doc.rollback( transactions[j] );
}
// Emit an undo event with the state to be rolled back
this.emit( 'undo', this.states[i] );
}
}
};
/**
*
*
* @method
*/
es.HistoryModel.prototype.redo = function( steps ) {
if ( steps === undefined ) {
steps = 1;
}
// Stop redo at the last state
var previousStateIndex = this.currentStateIndex;
this.currentStateIndex = Math.min( this.states.length - 1, this.currentStateIndex + steps );
if ( previousStateIndex < this.currentStateIndex ) {
for ( var i = previousStateIndex + 1; i >= this.currentStateIndex; i++ ) {
// Apply transaction to the document
var transactions = this.states[i].getTransactions();
for ( var j = 0; j < transactions.length; j++ ) {
this.doc.commit( transactions[j] );
}
// Emit an undo event with the state to be rolled back
this.emit( 'redo', this.states[i] );
}
}
};
/* Inheritance */
es.extendClass( es.HistoryModel, es.EventEmitter );