mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-30 00:55:00 +00:00
7465b670e1
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
182 lines
5.5 KiB
JavaScript
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 = [];
|
|
};
|