%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/
Upload File :
Create Path :
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;
};

Zerion Mini Shell 1.0