mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-09-27 12:16:51 +00:00
Cleanup/refactor/documentation work for ve.dm.DocumentSynchronizer
Change-Id: I5f70460e0d4496c91d92b83025fdf9ae1d9dece6
This commit is contained in:
parent
2747ff05c5
commit
44b311920e
|
@ -1,16 +1,14 @@
|
|||
/**
|
||||
* Creates an ve.dm.DocumentSynchronizer object.
|
||||
*
|
||||
* This object is a utility for collecting actions to be performed on the model tree
|
||||
* in multiple steps and then processing those actions in a single step.
|
||||
*
|
||||
* NOTE: An 'update' event is emitted for every node that is touched by the synchronizer in any
|
||||
* way, but only once for every node. Other events are emitted as we go.
|
||||
* This object is a utility for collecting actions to be performed on the model tree in multiple
|
||||
* steps as the linear model is modified my 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
|
||||
* - The linear model has already been updated for the pushed actions
|
||||
* - Actions are pushed in increasing offset order
|
||||
* - Actions are non-overlapping
|
||||
*
|
||||
* @class
|
||||
* @constructor
|
||||
|
@ -19,38 +17,130 @@
|
|||
ve.dm.DocumentSynchronizer = function( doc ) {
|
||||
// Properties
|
||||
this.document = doc;
|
||||
this.actions = [];
|
||||
this.actionQueue = [];
|
||||
this.eventQueue = [];
|
||||
};
|
||||
|
||||
/* Static Members */
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @member
|
||||
*/
|
||||
ve.dm.DocumentSynchronizer.synchronizers = {
|
||||
|
||||
/* Static Methods */
|
||||
|
||||
/**
|
||||
* Synchronizes an annotation action.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
* @param {Object} action
|
||||
*/
|
||||
'annotation': function( action ) {
|
||||
// Queue events for all leaf nodes covered by the range
|
||||
// TODO test me
|
||||
var i, selection = this.document.selectNodes( action.range, 'leaves' );
|
||||
for ( i = 0; i < selection.length; i++ ) {
|
||||
this.queueEvent( selection[i].node, 'annotation' );
|
||||
this.queueEvent( selection[i].node, 'update' );
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Synchronizes an attribute change action.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
* @param {Object} action
|
||||
*/
|
||||
'attributeChange': function( action ) {
|
||||
this.queueEvent( action.node, 'attributeChange', action.key, action.from, action.to );
|
||||
this.queueEvent( action.node, 'update' );
|
||||
},
|
||||
/**
|
||||
* Synchronizes a resize action.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
* @param {Object} action
|
||||
*/
|
||||
'resize': function( action ) {
|
||||
action.node.adjustLength( action.adjustment );
|
||||
this.queueEvent( action.node, 'update' );
|
||||
},
|
||||
/**
|
||||
* Synchronizes a rebuild action.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
* @param {Object} action
|
||||
*/
|
||||
'rebuild': function( action ) {
|
||||
// Find the nodes contained by oldRange
|
||||
var selection = this.document.selectNodes( action.oldRange, 'siblings' );
|
||||
if ( selection.length === 0 ) {
|
||||
// WTF? Nothing to rebuild, I guess. Whatever.
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO index of firstNode in parent should be in the selectNodes result
|
||||
var firstNode = selection[0].node,
|
||||
parent = firstNode.getParent(),
|
||||
index = parent.indexOf( firstNode );
|
||||
|
||||
this.document.rebuildNodes( parent, index, selection.length, action.oldRange.from,
|
||||
action.newRange.getLength()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Gets 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.
|
||||
* 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.actions.push( {
|
||||
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.
|
||||
* 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 from Old value of the attribute
|
||||
* @param to New value of the attribute
|
||||
* @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.actions.push( {
|
||||
this.actionQueue.push( {
|
||||
'type': 'attributeChange',
|
||||
'node': node,
|
||||
'key': key,
|
||||
|
@ -60,99 +150,99 @@ ve.dm.DocumentSynchronizer.prototype.pushAttributeChange = function( node, key,
|
|||
};
|
||||
|
||||
/**
|
||||
* Add a resize action to the queue. This changes the length of a text node.
|
||||
* 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 {Integer} adjustment Length adjustment to apply to the node
|
||||
*/
|
||||
ve.dm.DocumentSynchronizer.prototype.pushResize = function( node, adjustment ) {
|
||||
this.actions.push( {
|
||||
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.actions.push( {
|
||||
this.actionQueue.push( {
|
||||
'type': 'rebuild',
|
||||
'oldRange': oldRange,
|
||||
'newRange': newRange
|
||||
} );
|
||||
};
|
||||
|
||||
ve.dm.DocumentSynchronizer.prototype.annotation = function( action ) {
|
||||
// Queue events for all leaf nodes covered by the range
|
||||
// TODO test me
|
||||
var i, selection = this.document.selectNodes( action.range, 'leaves' );
|
||||
for ( i = 0; i < selection.length; i++ ) {
|
||||
this.queueEvent( selection[i].node, 'annotation' );
|
||||
this.queueEvent( selection[i].node, 'update' );
|
||||
}
|
||||
};
|
||||
|
||||
ve.dm.DocumentSynchronizer.prototype.attributeChange = function( action ) {
|
||||
this.queueEvent( action.node, 'attributeChange', action.key, action.from, action.to );
|
||||
this.queueEvent( action.node, 'update' );
|
||||
};
|
||||
|
||||
ve.dm.DocumentSynchronizer.prototype.resize = function( action ) {
|
||||
action.node.adjustLength( action.adjustment );
|
||||
this.queueEvent( action.node, 'update' );
|
||||
};
|
||||
|
||||
ve.dm.DocumentSynchronizer.prototype.rebuild = function( action ) {
|
||||
// Find the nodes contained by oldRange
|
||||
var selection = this.document.selectNodes( action.oldRange, 'siblings' );
|
||||
if ( selection.length === 0 ) {
|
||||
// WTF? Nothing to rebuild, I guess. Whatever.
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO index of firstNode in parent should be in the selectNodes result
|
||||
var firstNode = selection[0].node,
|
||||
parent = firstNode.getParent(),
|
||||
index = parent.indexOf( firstNode );
|
||||
|
||||
this.document.rebuildNodes( parent, index, selection.length, action.oldRange.from,
|
||||
action.newRange.getLength()
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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} [...] Additional arguments to be passed to the event when fired
|
||||
*/
|
||||
ve.dm.DocumentSynchronizer.prototype.queueEvent = function( node, event ) {
|
||||
// Check if this is already queued
|
||||
var args = Array.prototype.slice.call( arguments, 1 );
|
||||
var hash = $.toJSON( args );
|
||||
if ( !node.syncEventQueue ) {
|
||||
node.syncEventQueue = {};
|
||||
if ( !node.queuedEventHashes ) {
|
||||
node.queuedEventHashes = {};
|
||||
}
|
||||
if ( !node.syncEventQueue[hash] ) {
|
||||
node.syncEventQueue[hash] = true;
|
||||
if ( !node.queuedEventHashes[hash] ) {
|
||||
node.queuedEventHashes[hash] = true;
|
||||
this.eventQueue.push( { 'node': node, 'args': args } );
|
||||
}
|
||||
};
|
||||
|
||||
ve.dm.DocumentSynchronizer.prototype.emitEvents = function() {
|
||||
var i, event;
|
||||
for ( i = 0; i < this.eventQueue.length; i++ ) {
|
||||
event = this.eventQueue[i];
|
||||
event.node.emit.apply( event.node, event.args );
|
||||
delete event.node.syncEventQueue;
|
||||
}
|
||||
this.eventQueue = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Synchronizes 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, i;
|
||||
var action,
|
||||
event,
|
||||
i;
|
||||
// Execute the actions in the queue
|
||||
for ( i = 0; i < this.actions.length; i++ ) {
|
||||
action = this.actions[i];
|
||||
if ( action.type in this ) {
|
||||
this[action.type]( action );
|
||||
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 'Invalid action type ' + action.type;
|
||||
}
|
||||
}
|
||||
|
||||
this.emitEvents();
|
||||
this.actions = [];
|
||||
// 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 = [];
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue