mediawiki-extensions-Visual.../modules/ve/dm/ve.dm.DocumentSynchronizer.js
Catrope 7465b670e1 Add and update an offset map in DocumentNode
This has some TODOs still but I want to land it now anyway, and fix the
TODOs later.

* Add this.offsetMap which maps each linear model offset to a model tree node
* Refactor createNodesFromData()
** Rename it to buildSubtreeFromData()
** Have it build an offset map as well as a node subtree
** Have it set the root on the fake root node so that when the subtree
   is attached to the main tree later, we don't get a rippling root
   update all the way down
** Normalize the way the loop processes content, that way adding offsets
   for content is easier
* Add rebuildNodes() which uses buildSubtreeFromData() to rebuild stuff
* Use rebuildNodes() in DocumentSynchronizer
* Use pushRebuild() in TransactionProcessor
* Optimize setRoot() for the case where the root is already set correctly

Change-Id: I8b827d0823c969e671615ddd06e5f1bd70e9d54c
2012-04-13 16:46:02 -07:00

182 lines
5.5 KiB
JavaScript

/**
* 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.
*
* @class
* @constructor
*/
ve.dm.DocumentSynchronizer = function( model ) {
// Properties
this.model = model;
this.actions = [];
};
/* Methods */
ve.dm.DocumentSynchronizer.prototype.getModel = function() {
return this.model;
};
/**
* Add an insert action to the queue
* @param {ve.dm.BranchNode} node Node to insert
* @param {Integer} [offset] Offset of the inserted node, if known
*/
ve.dm.DocumentSynchronizer.prototype.pushInsert = function( node, offset ) {
this.actions.push( {
'type': 'insert',
'node': node,
'offset': offset || null
} );
};
/**
* Add a delete action to the queue
* @param {ve.dm.BranchNode} node Node to delete
*/
ve.dm.DocumentSynchronizer.prototype.pushDelete = function( node ) {
this.actions.push( {
'type': 'delete',
'node': node
} );
};
/**
* Add a rebuild action to the queue. This rebuilds one or more nodes from data
* found in the linear model.
* @param {ve.Range} oldRange Range that the old nodes used to span. This is
* used to find the old nodes in the model tree.
* @param {ve.Range} newRange Range that contains the new nodes. This is used
* to get the new node data from the linear model.
*/
ve.dm.DocumentSynchronizer.prototype.pushRebuild = function( oldRange, newRange ) {
oldRange.normalize();
newRange.normalize();
this.actions.push( {
'type': 'rebuild',
'oldRange': oldRange,
'newRange': newRange
} );
};
/**
* Add a resize action to the queue. This changes the content length of a leaf node.
* @param {ve.dm.BranchNode} 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( {
'type': 'resize',
'node': node,
'adjustment': adjustment
} );
};
/**
* Add an update action to the queue
* @param {ve.dm.BranchNode} node Node to update
*/
ve.dm.DocumentSynchronizer.prototype.pushUpdate = function( node ) {
this.actions.push( {
'type': 'update',
'node': node
} );
};
/**
* Apply queued actions to the model tree. This assumes that the linear model
* has already been updated, but the model tree has not yet been.
*
* @method
*/
ve.dm.DocumentSynchronizer.prototype.synchronize = function() {
// TODO: Normalize the actions list to clean up nested actions
// Perform all actions
var action,
offset,
parent;
for ( var i = 0, len = this.actions.length; i < len; i++ ) {
action = this.actions[i];
offset = action.offset || null;
switch ( action.type ) {
case 'insert':
// Compute the offset if it wasn't provided
if ( offset === null ) {
offset = this.model.getOffsetFromNode( action.node );
}
// Insert the new node at the given offset
var target = this.model.getNodeFromOffset( offset + 1 );
if ( target === this.model ) {
// Insert at the beginning of the document
this.model.splice( 0, 0, action.node );
} else if ( target === null ) {
// Insert at the end of the document
this.model.splice( this.model.getElementLength(), 0, action.node );
} else {
// Insert before the element currently at the offset
parent = target.getParent();
parent.splice( parent.indexOf( target ), 0, action.node );
}
break;
case 'delete':
// Replace original node with new node
parent = action.node.getParent();
parent.splice( parent.indexOf( action.node ), 1 );
break;
case 'rebuild':
// Find the node(s) contained by oldRange. This is done by repeatedly
// invoking selectNodes() in shallow mode until we find the right node(s).
// TODO this traversal could be made more efficient once we have an offset map
// TODO I need to add this recursive shallow stuff to selectNodes() as a 'siblings' mode
var selection, node = this.model, range = action.oldRange;
while ( true ) {
selection = node.selectNodes( range, true );
// We stop descending if:
// * we got more than one node, OR
// * we got a leaf node, OR
// * we got no range, which means the entire node is covered, OR
// * we got the same node back, which means we'd get in an infinite loop
if ( selection.length != 1 ||
!selection[0].node.hasChildren() ||
!selection[0].range ||
selection[0].node == node
) {
break;
}
// Descend into this node
node = selection[0].node;
range = selection[0].range;
}
if ( selection[0].node == this.model ) {
// We got some sort of weird input, ignore it
break;
}
// The first node we're rebuilding is selection[0].node , and we're rebuilding
// selection.length adjacent nodes.
// TODO selectNodes() discovers the index of selection[0].node in its parent,
// but discards it, and now we recompute it
this.model.rebuildNodes( selection[0].node.getParent(),
selection[0].node.getParent().indexOf( selection[0].node ),
selection.length, action.oldRange.from,
this.model.getData( action.newRange )
);
break;
case 'resize':
// Adjust node length - causes update events to be emitted
action.node.adjustContentLength( action.adjustment );
break;
case 'update':
// Emit update events
action.node.emit( 'update' );
break;
}
}
// We've processed the queue, clear it
this.actions = [];
};