mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-30 00:55:00 +00:00
72e06d9cbf
Previously, they were only being deduplicated based on the transaction, which meant that an undo was seen as a duplicate (but then if you undid again, that wasn't a duplicate). Change-Id: If432ea28e6c206a2ad5562e529e2d3ed808c20e4
301 lines
8.7 KiB
JavaScript
301 lines
8.7 KiB
JavaScript
/*!
|
|
* VisualEditor DataModel DocumentSynchronizer class.
|
|
*
|
|
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* DataModel document synchronizer.
|
|
*
|
|
* This object is a utility for collecting actions to be performed on the model tree in multiple
|
|
* steps as the linear model is modified by a transaction processor and then processing those queued
|
|
* actions when the transaction is done being processed.
|
|
*
|
|
* IMPORTANT NOTE: It is assumed that:
|
|
* - The linear model has already been updated for the pushed actions
|
|
* - Actions are pushed in increasing offset order
|
|
* - Actions are non-overlapping
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @param {ve.dm.Document} doc Document to synchronize
|
|
* @param {ve.dm.Transaction} transaction The transaction being synchronized for
|
|
*/
|
|
ve.dm.DocumentSynchronizer = function VeDmDocumentSynchronizer( doc, transaction ) {
|
|
// Properties
|
|
this.document = doc;
|
|
this.actionQueue = [];
|
|
this.eventQueue = [];
|
|
this.adjustment = 0;
|
|
this.transaction = transaction;
|
|
};
|
|
|
|
/* Static Properties */
|
|
|
|
/**
|
|
* Synchronization methods.
|
|
*
|
|
* Each method is specific to a type of action. Methods are called in the context of a document
|
|
* synchronizer, so they work similar to normal methods on the object.
|
|
*
|
|
* @static
|
|
* @property
|
|
*/
|
|
ve.dm.DocumentSynchronizer.synchronizers = {};
|
|
|
|
/* Static Methods */
|
|
|
|
/**
|
|
* Synchronize an annotation action.
|
|
*
|
|
* This method is called within the context of a document synchronizer instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} action
|
|
*/
|
|
ve.dm.DocumentSynchronizer.synchronizers.annotation = function ( action ) {
|
|
// Queue events for all leaf nodes covered by the range
|
|
var i,
|
|
adjustedRange = ve.Range.newFromTranslatedRange( action.range, this.adjustment ),
|
|
selection = this.document.selectNodes( adjustedRange, 'leaves' );
|
|
for ( i = 0; i < selection.length; i++ ) {
|
|
// No tree synchronization needed
|
|
// Queue events
|
|
this.queueEvent( selection[i].node, 'annotation' );
|
|
this.queueEvent( selection[i].node, 'update' );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Synchronize an attribute change action.
|
|
*
|
|
* This method is called within the context of a document synchronizer instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} action
|
|
*/
|
|
ve.dm.DocumentSynchronizer.synchronizers.attributeChange = function ( action ) {
|
|
// No tree synchronization needed
|
|
// Queue events
|
|
this.queueEvent( action.node, 'attributeChange', action.key, action.from, action.to );
|
|
this.queueEvent( action.node, 'update' );
|
|
};
|
|
|
|
/**
|
|
* Synchronize a resize action.
|
|
*
|
|
* This method is called within the context of a document synchronizer instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} action
|
|
*/
|
|
ve.dm.DocumentSynchronizer.synchronizers.resize = function ( action ) {
|
|
var node = action.node,
|
|
parent = node.getParent();
|
|
|
|
if ( parent && node.getType() === 'text' && node.getLength() + action.adjustment === 0 ) {
|
|
// Auto-prune empty text nodes
|
|
parent.splice( parent.indexOf( node ), 1 );
|
|
} else {
|
|
// Apply length change to tree
|
|
// No update event needed, adjustLength causes an update event on its own
|
|
// FIXME however, any queued update event will still be emitted, resulting in a duplicate
|
|
node.adjustLength( action.adjustment );
|
|
}
|
|
// Update adjustment
|
|
this.adjustment += action.adjustment;
|
|
};
|
|
|
|
/**
|
|
* Synchronize a rebuild action.
|
|
*
|
|
* This method is called within the context of a document synchronizer instance.
|
|
*
|
|
* @static
|
|
* @method
|
|
* @param {Object} action
|
|
*/
|
|
ve.dm.DocumentSynchronizer.synchronizers.rebuild = function ( action ) {
|
|
var firstNode, parent, index, numNodes,
|
|
// Find the nodes contained by oldRange
|
|
adjustedOldRange = ve.Range.newFromTranslatedRange( action.oldRange, this.adjustment ),
|
|
selection = this.document.selectNodes( adjustedOldRange, 'siblings' );
|
|
|
|
// If the document is empty, selection[0].node will be the document (so no parent)
|
|
// but we won't get indexInNode either. Detect this and use index=0 in that case.
|
|
if ( 'indexInNode' in selection[0] || !selection[0].node.getParent() ) {
|
|
// Insertion
|
|
parent = selection[0].node;
|
|
index = selection[0].indexInNode || 0;
|
|
numNodes = 0;
|
|
} else {
|
|
// Rebuild
|
|
firstNode = selection[0].node;
|
|
parent = firstNode.getParent();
|
|
index = selection[0].index;
|
|
numNodes = selection.length;
|
|
}
|
|
// Perform rebuild in tree
|
|
this.document.rebuildNodes( parent, index, numNodes, adjustedOldRange.from,
|
|
action.newRange.getLength()
|
|
);
|
|
// Update adjustment
|
|
this.adjustment += action.newRange.getLength() - adjustedOldRange.getLength();
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Get the document being synchronized.
|
|
*
|
|
* @method
|
|
* @returns {ve.dm.Document} Document being synchronized
|
|
*/
|
|
ve.dm.DocumentSynchronizer.prototype.getDocument = function () {
|
|
return this.document;
|
|
};
|
|
|
|
/**
|
|
* Add an annotation action to the queue.
|
|
*
|
|
* This finds all leaf nodes covered wholly or partially by the given range, and emits annotation
|
|
* events for all of them.
|
|
*
|
|
* @method
|
|
* @param {ve.Range} range Range that was annotated
|
|
*/
|
|
ve.dm.DocumentSynchronizer.prototype.pushAnnotation = function ( range ) {
|
|
this.actionQueue.push( {
|
|
'type': 'annotation',
|
|
'range': range
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Add an attribute change to the queue.
|
|
*
|
|
* This emits an attributeChange event for the given node with the provided metadata.
|
|
*
|
|
* @method
|
|
* @param {ve.dm.Node} node Node whose attribute changed
|
|
* @param {string} key Key of the attribute that changed
|
|
* @param {Mixed} from Old value of the attribute
|
|
* @param {Mixed} to New value of the attribute
|
|
*/
|
|
ve.dm.DocumentSynchronizer.prototype.pushAttributeChange = function ( node, key, from, to ) {
|
|
this.actionQueue.push( {
|
|
'type': 'attributeChange',
|
|
'node': node,
|
|
'key': key,
|
|
'from': from,
|
|
'to': to
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Add a resize action to the queue.
|
|
*
|
|
* This changes the length of a text node.
|
|
*
|
|
* @method
|
|
* @param {ve.dm.TextNode} node Node to resize
|
|
* @param {number} adjustment Length adjustment to apply to the node
|
|
*/
|
|
ve.dm.DocumentSynchronizer.prototype.pushResize = function ( node, adjustment ) {
|
|
this.actionQueue.push( {
|
|
'type': 'resize',
|
|
'node': node,
|
|
'adjustment': adjustment
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Add a rebuild action to the queue.
|
|
*
|
|
* When a range of data has been changed arbitrarily this can be used to drop the nodes that
|
|
* represented the original range and replace them with new nodes that represent the new range.
|
|
*
|
|
* @method
|
|
* @param {ve.Range} oldRange Range of old nodes to be dropped
|
|
* @param {ve.Range} newRange Range for new nodes to be built from
|
|
*/
|
|
ve.dm.DocumentSynchronizer.prototype.pushRebuild = function ( oldRange, newRange ) {
|
|
this.actionQueue.push( {
|
|
'type': 'rebuild',
|
|
'oldRange': oldRange,
|
|
'newRange': newRange
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Queue an event to be emitted on a node.
|
|
*
|
|
* This method is called by methods defined in {ve.dm.DocumentSynchronizer.synchronizers}.
|
|
*
|
|
* Duplicate events will be ignored only if all arguments match exactly. Hashes of each event that
|
|
* has been queued are stored in the nodes they will eventually be fired on.
|
|
*
|
|
* @method
|
|
* @param {ve.dm.Node} node
|
|
* @param {string} event Event name
|
|
* @param {Mixed...} [args] Additional arguments to be passed to the event when fired
|
|
*/
|
|
ve.dm.DocumentSynchronizer.prototype.queueEvent = function ( node ) {
|
|
// Check if this is already queued
|
|
var
|
|
args = Array.prototype.slice.call( arguments, 1 ),
|
|
hash = ve.getHash( args );
|
|
|
|
if ( !node.queuedEventHashes ) {
|
|
node.queuedEventHashes = {};
|
|
}
|
|
if ( !node.queuedEventHashes[hash] ) {
|
|
node.queuedEventHashes[hash] = true;
|
|
this.eventQueue.push( {
|
|
'node': node,
|
|
'args': args.concat( this.transaction )
|
|
} );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Synchronize node tree using queued actions.
|
|
*
|
|
* This method uses the static methods defined in {ve.dm.DocumentSynchronizer.synchronizers} and
|
|
* calls them in the context of {this}.
|
|
*
|
|
* After synchronization is complete all queued events will be emitted. Hashes of queued events that
|
|
* have been stored on nodes are removed from the nodes after the events have all been emitted.
|
|
*
|
|
* This method also clears both action and event queues.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.dm.DocumentSynchronizer.prototype.synchronize = function () {
|
|
var action,
|
|
event,
|
|
i;
|
|
// Execute the actions in the queue
|
|
for ( i = 0; i < this.actionQueue.length; i++ ) {
|
|
action = this.actionQueue[i];
|
|
if ( action.type in ve.dm.DocumentSynchronizer.synchronizers ) {
|
|
ve.dm.DocumentSynchronizer.synchronizers[action.type].call( this, action );
|
|
} else {
|
|
throw new Error( 'Invalid action type ' + action.type );
|
|
}
|
|
}
|
|
// Emit events in the event queue
|
|
for ( i = 0; i < this.eventQueue.length; i++ ) {
|
|
event = this.eventQueue[i];
|
|
event.node.emit.apply( event.node, event.args );
|
|
delete event.node.queuedEventHashes;
|
|
}
|
|
// Clear queues
|
|
this.actionQueue = [];
|
|
this.eventQueue = [];
|
|
};
|