%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/ve.dm.Surface.js |
/*!
* VisualEditor DataModel Surface class.
*
* @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org
*/
/**
* DataModel surface.
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
* @param {ve.dm.Document} doc Document model to create surface for
*/
ve.dm.Surface = function VeDmSurface( doc ) {
// Mixin constructors
OO.EventEmitter.call( this );
// Properties
this.documentModel = doc;
this.metaList = new ve.dm.MetaList( this );
this.selection = new ve.dm.NullSelection( this.getDocument() );
this.selectionBefore = new ve.dm.NullSelection( this.getDocument() );
this.translatedSelection = null;
this.branchNodes = {};
this.selectedNode = null;
this.newTransactions = [];
this.stagingStack = [];
this.undoStack = [];
this.undoIndex = 0;
this.historyTrackingInterval = null;
this.insertionAnnotations = new ve.dm.AnnotationSet( this.getDocument().getStore() );
this.selectedAnnotations = new ve.dm.AnnotationSet( this.getDocument().getStore() );
this.isCollapsed = null;
this.enabled = true;
this.transacting = false;
this.queueingContextChanges = false;
this.contextChangeQueued = false;
// Events
this.getDocument().connect( this, {
transact: 'onDocumentTransact',
precommit: 'onDocumentPreCommit',
presynchronize: 'onDocumentPreSynchronize'
} );
};
/* Inheritance */
OO.mixinClass( ve.dm.Surface, OO.EventEmitter );
/* Events */
/**
* @event select
* @param {ve.dm.Selection} selection
*/
/**
* @event focus
*
* The selection was just set to a non-null selection
*/
/**
* @event blur
*
* The selection was just set to a null selection
*/
/**
* @event documentUpdate
*
* Emitted when a transaction has been processed on the document and the selection has been
* translated to account for that transaction. You should only use this event if you need
* to access the selection; in most cases, you should use {ve.dm.Document#event-transact}.
*
* @param {ve.dm.Transaction} tx Transaction that was processed on the document
*/
/**
* @event contextChange
*/
/**
* @event insertionAnnotationsChange
* @param {ve.dm.AnnotationSet} insertionAnnotations AnnotationSet being inserted
*/
/**
* @event history
*/
/* Methods */
/**
* Disable changes.
*
* @fires history
*/
ve.dm.Surface.prototype.disable = function () {
this.stopHistoryTracking();
this.enabled = false;
this.emit( 'history' );
};
/**
* Enable changes.
*
* @fires history
*/
ve.dm.Surface.prototype.enable = function () {
this.enabled = true;
this.startHistoryTracking();
this.emit( 'history' );
};
/**
* Initialize the surface model
*
* @fires contextChange
*/
ve.dm.Surface.prototype.initialize = function () {
this.startHistoryTracking();
this.emit( 'contextChange' );
};
/**
* Start tracking state changes in history.
*/
ve.dm.Surface.prototype.startHistoryTracking = function () {
if ( !this.enabled ) {
return;
}
if ( this.historyTrackingInterval === null ) {
this.historyTrackingInterval = setInterval( this.breakpoint.bind( this ), 3000 );
}
};
/**
* Stop tracking state changes in history.
*/
ve.dm.Surface.prototype.stopHistoryTracking = function () {
if ( !this.enabled ) {
return;
}
if ( this.historyTrackingInterval !== null ) {
clearInterval( this.historyTrackingInterval );
this.historyTrackingInterval = null;
}
};
/**
* Reset the timer for automatic history-tracking
*/
ve.dm.Surface.prototype.resetHistoryTrackingInterval = function () {
this.stopHistoryTracking();
this.startHistoryTracking();
};
/**
* Get a list of all applied history states.
*
* @return {Object[]} List of applied transaction stacks
*/
ve.dm.Surface.prototype.getHistory = function () {
var appliedUndoStack = this.undoStack.slice( 0, this.undoStack.length - this.undoIndex );
if ( this.newTransactions.length > 0 ) {
return appliedUndoStack.concat( [ { transactions: this.newTransactions.slice( 0 ) } ] );
}
return appliedUndoStack;
};
/**
* If the surface in staging mode.
*
* @return {boolean} The surface in staging mode
*/
ve.dm.Surface.prototype.isStaging = function () {
return this.stagingStack.length > 0;
};
/**
* Get the staging state at the current staging stack depth
*
* @return {Object|undefined} staging Staging state object, or undefined if not staging
* @return {ve.dm.Transaction[]} staging.transactions Staging transactions
* @return {ve.dm.Selection} staging.selectionBefore Selection before transactions were applied
* @return {boolean} staging.allowUndo Allow undo while staging
*/
ve.dm.Surface.prototype.getStaging = function () {
return this.stagingStack[ this.stagingStack.length - 1 ];
};
/**
* Undo is allowed at the current staging stack depth
*
* @return {boolean|undefined} Undo is allowed, or undefined if not staging
*/
ve.dm.Surface.prototype.doesStagingAllowUndo = function () {
var staging = this.getStaging();
return staging && staging.allowUndo;
};
/**
* Get the staging transactions at the current staging stack depth
*
* The array is returned by reference so it can be pushed to.
*
* @return {ve.dm.Transaction[]|undefined} Staging transactions, or undefined if not staging
*/
ve.dm.Surface.prototype.getStagingTransactions = function () {
var staging = this.getStaging();
return staging && staging.transactions;
};
/**
* Push another level of staging to the staging stack
*
* @param {boolean} [allowUndo=false] Allow undo while staging
*/
ve.dm.Surface.prototype.pushStaging = function ( allowUndo ) {
// If we're starting staging stop history tracking
if ( !this.isStaging() ) {
// Set a breakpoint to make sure newTransactions is clear
this.breakpoint();
this.stopHistoryTracking();
this.emit( 'history' );
}
this.stagingStack.push( {
transactions: [],
selectionBefore: new ve.dm.NullSelection( this.getDocument() ),
allowUndo: !!allowUndo
} );
};
/**
* Pop a level of staging from the staging stack
*
* @fires history
* @return {ve.dm.Transaction[]|undefined} Staging transactions, or undefined if not staging
*/
ve.dm.Surface.prototype.popStaging = function () {
var i, transaction, staging, transactions,
reverseTransactions = [];
if ( !this.isStaging() ) {
return;
}
staging = this.stagingStack.pop();
transactions = staging.transactions;
// Not applying, so rollback transactions
for ( i = transactions.length - 1; i >= 0; i-- ) {
transaction = transactions[ i ].reversed();
reverseTransactions.push( transaction );
}
this.changeInternal( reverseTransactions, undefined, true );
if ( !this.isStaging() ) {
this.startHistoryTracking();
this.emit( 'history' );
}
return transactions;
};
/**
* Apply a level of staging from the staging stack
*
* @fires history
*/
ve.dm.Surface.prototype.applyStaging = function () {
var staging;
if ( !this.isStaging() ) {
return;
}
staging = this.stagingStack.pop();
if ( this.isStaging() ) {
// Merge popped transactions into the current item in the staging stack
ve.batchPush( this.getStagingTransactions(), staging.transactions );
// If the current level has a null selectionBefore, copy that over too
if ( this.getStaging().selectionBefore.isNull() ) {
this.getStaging().selectionBefore = staging.selectionBefore;
}
} else {
this.truncateUndoStack();
// Move transactions to the undo stack
this.newTransactions = staging.transactions;
this.selectionBefore = staging.selectionBefore;
this.breakpoint();
}
if ( !this.isStaging() ) {
this.startHistoryTracking();
this.emit( 'history' );
}
};
/**
* Pop the staging stack until empty
*
* @return {ve.dm.Transaction[]|undefined} Staging transactions, or undefined if not staging
*/
ve.dm.Surface.prototype.popAllStaging = function () {
var transactions = [];
if ( !this.isStaging() ) {
return;
}
while ( this.isStaging() ) {
ve.batchSplice( transactions, 0, 0, this.popStaging() );
}
return transactions;
};
/**
* Apply the staging stack until empty
*/
ve.dm.Surface.prototype.applyAllStaging = function () {
while ( this.isStaging() ) {
this.applyStaging();
}
};
/**
* Get annotations that will be used upon insertion.
*
* @return {ve.dm.AnnotationSet} Insertion annotations
*/
ve.dm.Surface.prototype.getInsertionAnnotations = function () {
return this.insertionAnnotations.clone();
};
/**
* Set annotations that will be used upon insertion.
*
* @param {ve.dm.AnnotationSet|null} annotations Insertion annotations to use or null to disable them
* @fires insertionAnnotationsChange
* @fires contextChange
*/
ve.dm.Surface.prototype.setInsertionAnnotations = function ( annotations ) {
if ( !this.enabled ) {
return;
}
this.insertionAnnotations = annotations !== null ?
annotations.clone() :
new ve.dm.AnnotationSet( this.getDocument().getStore() );
this.emit( 'insertionAnnotationsChange', this.insertionAnnotations );
this.emit( 'contextChange' );
};
/**
* Add an annotation to be used upon insertion.
*
* @param {ve.dm.Annotation|ve.dm.AnnotationSet} annotations Insertion annotation to add
* @fires insertionAnnotationsChange
* @fires contextChange
*/
ve.dm.Surface.prototype.addInsertionAnnotations = function ( annotations ) {
if ( !this.enabled ) {
return;
}
if ( annotations instanceof ve.dm.Annotation ) {
this.insertionAnnotations.push( annotations );
} else if ( annotations instanceof ve.dm.AnnotationSet ) {
this.insertionAnnotations.addSet( annotations );
} else {
throw new Error( 'Invalid annotations' );
}
this.emit( 'insertionAnnotationsChange', this.insertionAnnotations );
this.emit( 'contextChange' );
};
/**
* Remove an annotation from those that will be used upon insertion.
*
* @param {ve.dm.Annotation|ve.dm.AnnotationSet} annotations Insertion annotation to remove
* @fires insertionAnnotationsChange
* @fires contextChange
*/
ve.dm.Surface.prototype.removeInsertionAnnotations = function ( annotations ) {
if ( !this.enabled ) {
return;
}
if ( annotations instanceof ve.dm.Annotation ) {
this.insertionAnnotations.remove( annotations );
} else if ( annotations instanceof ve.dm.AnnotationSet ) {
this.insertionAnnotations.removeSet( annotations );
} else {
throw new Error( 'Invalid annotations' );
}
this.emit( 'insertionAnnotationsChange', this.insertionAnnotations );
this.emit( 'contextChange' );
};
/**
* Check if redo is allowed in the current state.
*
* @return {boolean} Redo is allowed
*/
ve.dm.Surface.prototype.canRedo = function () {
return this.undoIndex > 0 && this.enabled;
};
/**
* Check if undo is allowed in the current state.
*
* @return {boolean} Undo is allowed
*/
ve.dm.Surface.prototype.canUndo = function () {
return this.hasBeenModified() && this.enabled && ( !this.isStaging() || this.doesStagingAllowUndo() );
};
/**
* Check if the surface has been modified.
*
* This only checks if there are transactions which haven't been undone.
*
* @return {boolean} The surface has been modified
*/
ve.dm.Surface.prototype.hasBeenModified = function () {
return this.undoStack.length - this.undoIndex > 0 || !!this.newTransactions.length;
};
/**
* Get the document model.
*
* @return {ve.dm.Document} Document model of the surface
*/
ve.dm.Surface.prototype.getDocument = function () {
return this.documentModel;
};
/**
* Get the meta list.
*
* @return {ve.dm.MetaList} Meta list of the surface
*/
ve.dm.Surface.prototype.getMetaList = function () {
return this.metaList;
};
/**
* Get the selection.
*
* @return {ve.dm.Selection} Current selection
*/
ve.dm.Surface.prototype.getSelection = function () {
return this.selection;
};
/**
* Get the selection translated for the transaction that's being committed, if any.
*
* @return {ve.dm.Selection} Current selection translated for new transaction
*/
ve.dm.Surface.prototype.getTranslatedSelection = function () {
return this.translatedSelection || this.selection;
};
/**
* Get a fragment for a selection.
*
* @param {ve.dm.Selection} [selection] Selection within target document, current selection used by default
* @param {boolean} [noAutoSelect] Don't update the surface's selection when making changes
* @param {boolean} [excludeInsertions] Exclude inserted content at the boundaries when updating range
* @return {ve.dm.SurfaceFragment} Surface fragment
*/
ve.dm.Surface.prototype.getFragment = function ( selection, noAutoSelect, excludeInsertions ) {
return new ve.dm.SurfaceFragment( this, selection || this.selection, noAutoSelect, excludeInsertions );
};
/**
* Get a fragment for a linear selection's range.
*
* @param {ve.Range} range Selection's range
* @param {boolean} [noAutoSelect] Don't update the surface's selection when making changes
* @param {boolean} [excludeInsertions] Exclude inserted content at the boundaries when updating range
* @return {ve.dm.SurfaceFragment} Surface fragment
*/
ve.dm.Surface.prototype.getLinearFragment = function ( range, noAutoSelect, excludeInsertions ) {
return this.getFragment( new ve.dm.LinearSelection( this.getDocument(), range ), noAutoSelect, excludeInsertions );
};
/**
* Prevent future states from being redone.
*
* Callers should eventually emit a 'history' event after using this method.
*/
ve.dm.Surface.prototype.truncateUndoStack = function () {
if ( this.undoIndex ) {
this.undoStack = this.undoStack.slice( 0, this.undoStack.length - this.undoIndex );
this.undoIndex = 0;
}
};
/**
* Start queueing up calls to #emitContextChange until #stopQueueingContextChanges is called.
* While queueing is active, contextChanges are also collapsed, so if #emitContextChange is called
* multiple times, only one contextChange event will be emitted by #stopQueueingContextChanges.
*
* this.emitContextChange(); // emits immediately
* this.startQueueingContextChanges();
* this.emitContextChange(); // doesn't emit
* this.emitContextChange(); // doesn't emit
* this.stopQueueingContextChanges(); // emits one contextChange event
*
* @private
*/
ve.dm.Surface.prototype.startQueueingContextChanges = function () {
if ( !this.queueingContextChanges ) {
this.queueingContextChanges = true;
this.contextChangeQueued = false;
}
};
/**
* Emit a contextChange event. If #startQueueingContextChanges has been called, then the event
* is deferred until #stopQueueingContextChanges is called.
*
* @private
* @fires contextChange
*/
ve.dm.Surface.prototype.emitContextChange = function () {
if ( this.queueingContextChanges ) {
this.contextChangeQueued = true;
} else {
this.emit( 'contextChange' );
}
};
/**
* Stop queueing contextChange events. If #emitContextChange was called previously, a contextChange
* event will now be emitted. Any future calls to #emitContextChange will once again emit the
* event immediately.
*
* @private
* @fires contextChange
*/
ve.dm.Surface.prototype.stopQueueingContextChanges = function () {
if ( this.queueingContextChanges ) {
this.queueingContextChanges = false;
if ( this.contextChangeQueued ) {
this.contextChangeQueued = false;
this.emit( 'contextChange' );
}
}
};
/**
* Set a linear selection at a specified range on the model
*
* @param {ve.Range} range Range to create linear selection at
*/
ve.dm.Surface.prototype.setLinearSelection = function ( range ) {
this.setSelection( new ve.dm.LinearSelection( this.getDocument(), range ) );
};
/**
* Set a null selection on the model
*/
ve.dm.Surface.prototype.setNullSelection = function () {
this.setSelection( new ve.dm.NullSelection( this.getDocument() ) );
};
/**
* Grows a range so that any partially selected links are totally selected
*
* @param {ve.Range} range The range to regularize
* @return {ve.Range} Regularized range, possibly object-identical to the original
*/
ve.dm.Surface.prototype.fixupRangeForLinks = function ( range ) {
var rangeAnnotations, startLink, endLink,
linearData = this.getDocument().data,
start = range.start,
end = range.end;
function getLinks( offset ) {
return linearData.getAnnotationsFromOffset( offset ).filter( function ( ann ) {
return ann.name === 'link';
} );
}
if ( range.isCollapsed() ) {
return range;
}
// Search for links at start/end that don't cover the whole range.
// Assume at most one such link at each end.
rangeAnnotations = linearData.getAnnotationsFromRange( range );
startLink = getLinks( start ).diffWith( rangeAnnotations ).getIndex( 0 );
endLink = getLinks( end ).diffWith( rangeAnnotations ).getIndex( 0 );
if ( startLink === undefined && endLink === undefined ) {
return range;
}
if ( startLink !== undefined ) {
while ( start > 0 && getLinks( start - 1 ).containsIndex( startLink ) ) {
start--;
}
}
if ( endLink !== undefined ) {
while ( end < linearData.getLength() && getLinks( end ).containsIndex( endLink ) ) {
end++;
}
}
if ( range.isBackwards() ) {
return new ve.Range( end, start );
} else {
return new ve.Range( start, end );
}
};
/**
* Change the selection
*
* @param {ve.dm.Selection} selection New selection
*
* @fires select
* @fires contextChange
*/
ve.dm.Surface.prototype.setSelection = function ( selection ) {
var insertionAnnotations, selectedNode, range, selectedAnnotations,
oldSelection = this.selection,
branchNodes = {},
selectionChange = false,
contextChange = false,
linearData = this.getDocument().data;
if ( !this.enabled ) {
return;
}
this.translatedSelection = null;
if ( this.transacting ) {
// Update the selection but don't do any processing
this.selection = selection;
return;
}
// this.selection needs to be updated before we call setInsertionAnnotations
if ( !oldSelection.equals( selection ) ) {
selectionChange = true;
this.selection = selection;
}
if ( selection instanceof ve.dm.LinearSelection ) {
range = selection.getRange();
// Update branch nodes
branchNodes.start = this.getDocument().getBranchNodeFromOffset( range.start );
if ( !range.isCollapsed() ) {
branchNodes.end = this.getDocument().getBranchNodeFromOffset( range.end );
} else {
branchNodes.end = branchNodes.start;
}
selectedNode = this.getSelectedNodeFromSelection( selection );
// Reset insertionAnnotations based on the neighbouring document data
insertionAnnotations = linearData.getInsertionAnnotationsFromRange( range );
// If there's *any* difference in insertion annotations (even order), then:
// * emit insertionAnnotationsChange
// * emit contextChange (TODO: is this desirable?)
if ( !insertionAnnotations.equalsInOrder( this.insertionAnnotations ) ) {
this.setInsertionAnnotations( insertionAnnotations );
}
// Reset selectedAnnotations
if ( range.isCollapsed() ) {
selectedAnnotations = linearData.getAnnotationsFromOffset( range.start );
} else {
selectedAnnotations = linearData.getAnnotationsFromRange( range, true );
}
if ( !selectedAnnotations.compareTo( this.selectedAnnotations ) ) {
this.selectedAnnotations = selectedAnnotations;
contextChange = true;
}
} else if ( selection instanceof ve.dm.TableSelection ) {
selectedNode = selection.getMatrixCells()[ 0 ].node;
contextChange = true;
} else if ( selection instanceof ve.dm.NullSelection ) {
contextChange = true;
}
if ( range && range.isCollapsed() !== this.isCollapsed ) {
// selectedAnnotations won't have changed if going from insertion annotations to
// selection of the same annotations, but some tools will consider that a context change
// (e.g. ClearAnnotationTool).
this.isCollapsed = range.isCollapsed();
contextChange = true;
}
// If branchNodes or selectedNode changed emit a contextChange
if (
selectedNode !== this.selectedNode ||
branchNodes.start !== this.branchNodes.start ||
branchNodes.end !== this.branchNodes.end
) {
this.branchNodes = branchNodes;
this.selectedNode = selectedNode;
contextChange = true;
}
// If selection changed emit a select
if ( selectionChange ) {
this.emit( 'select', this.selection.clone() );
if ( oldSelection.isNull() ) {
this.emit( 'focus' );
}
if ( selection.isNull() ) {
this.emit( 'blur' );
}
}
if ( contextChange ) {
this.emitContextChange();
}
};
/**
* Place the selection at the first content offset in the document.
*/
ve.dm.Surface.prototype.selectFirstContentOffset = function () {
var firstOffset = this.getDocument().data.getNearestContentOffset( 0, 1 );
if ( firstOffset !== -1 ) {
// Found a content offset
this.setLinearSelection( new ve.Range( firstOffset ) );
} else {
// Document is full of structural nodes, just give up
this.setNullSelection();
}
};
/**
* Place the selection at the last content offset in the document.
*/
ve.dm.Surface.prototype.selectLastContentOffset = function () {
var data = this.getDocument().data,
listOffset = this.getDocument().getInternalList().getListNode().getOuterRange().start,
lastOffset = data.getNearestContentOffset( listOffset, -1 );
if ( lastOffset !== -1 ) {
// Found a content offset
this.setLinearSelection( new ve.Range( lastOffset ) );
} else {
// Document is full of structural nodes, just give up
this.setNullSelection();
}
};
/**
* Apply a transactions and selection changes to the document.
*
* @param {ve.dm.Transaction|ve.dm.Transaction[]|null} transactions One or more transactions to
* process, or null to process none
* @param {ve.dm.Selection} [selection] Selection to apply
* @fires contextChange
*/
ve.dm.Surface.prototype.change = function ( transactions, selection ) {
this.changeInternal( transactions, selection, false );
};
/**
* Internal implementation of change(). Do not use this, use change() instead.
*
* @private
* @param {ve.dm.Transaction|ve.dm.Transaction[]|null} transactions
* @param {ve.dm.Selection} [selection] [selection]
* @param {boolean} [skipUndoStack=false] If true, do not modify the undo stack. Used by undo/redo
* @fires select
* @fires history
* @fires contextChange
*/
ve.dm.Surface.prototype.changeInternal = function ( transactions, selection, skipUndoStack ) {
var i, len, selectionAfter,
selectionBefore = this.selection.clone(),
contextChange = false;
if ( !this.enabled ) {
return;
}
this.startQueueingContextChanges();
// Process transactions
if ( transactions ) {
if ( transactions instanceof ve.dm.Transaction ) {
transactions = [ transactions ];
}
this.transacting = true;
for ( i = 0, len = transactions.length; i < len; i++ ) {
if ( !transactions[ i ].isNoOp() ) {
if ( !skipUndoStack ) {
if ( this.isStaging() ) {
if ( !this.getStagingTransactions().length ) {
this.getStaging().selectionBefore = selectionBefore;
}
this.getStagingTransactions().push( transactions[ i ] );
} else {
this.truncateUndoStack();
if ( !this.newTransactions.length ) {
this.selectionBefore = selectionBefore;
}
this.newTransactions.push( transactions[ i ] );
}
}
// The .commit() call below indirectly invokes setSelection()
this.getDocument().commit( transactions[ i ], this.isStaging() );
if ( transactions[ i ].hasElementAttributeOperations() ) {
contextChange = true;
}
}
}
this.transacting = false;
this.emit( 'history' );
}
selectionAfter = this.selection;
// Apply selection change
if ( selection ) {
this.setSelection( selection );
} else if ( transactions ) {
// Call setSelection() to trigger selection processing that was bypassed earlier
this.setSelection( this.selection );
}
// If the selection changed while applying the transactions but not while applying the
// selection change, setSelection() won't have emitted a 'select' event. We don't want that
// to happen, so emit one anyway.
if (
!selectionBefore.equals( selectionAfter ) &&
selectionAfter.equals( this.selection )
) {
this.emit( 'select', this.selection.clone() );
}
if ( contextChange ) {
this.emitContextChange();
}
this.stopQueueingContextChanges();
};
/**
* Set a history state breakpoint.
*
* @return {boolean} A breakpoint was added
*/
ve.dm.Surface.prototype.breakpoint = function () {
if ( !this.enabled ) {
return false;
}
this.resetHistoryTrackingInterval();
if ( this.newTransactions.length > 0 ) {
this.undoStack.push( {
transactions: this.newTransactions,
selection: this.selection.clone(),
selectionBefore: this.selectionBefore.clone()
} );
this.newTransactions = [];
return true;
} else if ( this.selectionBefore.isNull() && !this.selection.isNull() ) {
this.selectionBefore = this.selection.clone();
}
return false;
};
/**
* Step backwards in history.
*/
ve.dm.Surface.prototype.undo = function () {
var i, item, transaction, transactions = [];
if ( !this.canUndo() ) {
return;
}
if ( this.isStaging() ) {
this.popAllStaging();
}
this.breakpoint();
this.undoIndex++;
item = this.undoStack[ this.undoStack.length - this.undoIndex ];
if ( item ) {
// Apply reversed transactions in reversed order
for ( i = item.transactions.length - 1; i >= 0; i-- ) {
transaction = item.transactions[ i ].reversed();
transactions.push( transaction );
}
this.changeInternal( transactions, item.selectionBefore, true );
}
};
/**
* Step forwards in history.
*/
ve.dm.Surface.prototype.redo = function () {
var item;
if ( !this.canRedo() ) {
return;
}
this.breakpoint();
item = this.undoStack[ this.undoStack.length - this.undoIndex ];
if ( item ) {
this.undoIndex--;
// ve.copy( item.transactions ) invokes .clone() on each transaction in item.transactions
this.changeInternal( ve.copy( item.transactions ), item.selection, true );
}
};
/**
* Respond to transactions processed on the document by translating the selection and updating
* other state.
*
* @param {ve.dm.Transaction} tx Transaction that was processed
* @fires documentUpdate
*/
ve.dm.Surface.prototype.onDocumentTransact = function ( tx ) {
this.setSelection( this.getSelection().translateByTransaction( tx ) );
this.emit( 'documentUpdate', tx );
};
/**
* Get the cached selected node covering the current selection, or null
*
* @return {ve.dm.Node|null} Selected node
*/
ve.dm.Surface.prototype.getSelectedNode = function () {
return this.selectedNode;
};
/**
* Get the selected node covering a specific selection, or null
*
* @param {ve.dm.Selection} selection Selection
* @return {ve.dm.Node|null} Selected node
*/
ve.dm.Surface.prototype.getSelectedNodeFromSelection = function ( selection ) {
var range, startNode,
selectedNode = null;
selection = selection || this.getSelection();
if ( !( selection instanceof ve.dm.LinearSelection ) ) {
return null;
}
range = selection.getRange();
if ( !range.isCollapsed() ) {
startNode = this.getDocument().documentNode.getNodeFromOffset( range.start + 1 );
if ( startNode && startNode.getOuterRange().equalsSelection( range ) ) {
selectedNode = startNode;
}
}
return selectedNode;
};
/**
* Clone the selection ready for early translation (before synchronization).
*
* This is so #ve.ce.ContentBranchNode.getRenderedContents can consider the translated
* selection for unicorn rendering.
*/
ve.dm.Surface.prototype.onDocumentPreCommit = function () {
this.translatedSelection = this.selection.clone();
};
/**
* Update translatedSelection early (before synchronization)
*
* @param {ve.dm.Transaction} tx Transaction that was processed
* @fires documentUpdate
*/
ve.dm.Surface.prototype.onDocumentPreSynchronize = function ( tx ) {
if ( this.translatedSelection ) {
this.translatedSelection = this.translatedSelection.translateByTransaction( tx );
}
};
/**
* Get a minimal set of ranges which have been modified by changes to the surface.
*
* @return {ve.Range[]} Modified ranges
*/
ve.dm.Surface.prototype.getModifiedRanges = function () {
var ranges = [],
compactRanges = [],
lastRange = null;
this.getHistory().forEach( function ( stackItem ) {
stackItem.transactions.forEach( function ( tx ) {
var newRange = tx.getModifiedRange( this.documentModel );
// newRange will by null for no-ops
if ( newRange ) {
// Translate previous ranges by the current transaction
ranges.forEach( function ( range, i, arr ) {
arr[ i ] = tx.translateRange( range, true );
} );
if ( !newRange.isCollapsed() ) {
ranges.push( newRange );
}
}
} );
} );
ranges.sort( function ( a, b ) { return a.start - b.start; } ).forEach( function ( range ) {
if ( !range.isCollapsed() ) {
if ( lastRange && lastRange.touchesRange( range ) ) {
compactRanges.pop();
range = lastRange.expand( range );
}
compactRanges.push( range );
lastRange = range;
}
} );
return compactRanges;
};