2011-11-14 23:04:36 +00:00
|
|
|
/**
|
|
|
|
* Creates an es.TransactionProcessor object.
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
es.TransactionProcessor = function( model, transaction ) {
|
|
|
|
this.model = model;
|
|
|
|
this.transaction = transaction;
|
|
|
|
this.cursor = 0;
|
|
|
|
this.set = [];
|
|
|
|
this.clear = [];
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Static Members */
|
|
|
|
|
|
|
|
es.TransactionProcessor.operationMap = {
|
|
|
|
// Retain
|
|
|
|
'retain': {
|
|
|
|
'commit': function( op ) {
|
|
|
|
this.retain( op );
|
|
|
|
},
|
|
|
|
'rollback': function( op ) {
|
|
|
|
this.retain( op );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Insert
|
|
|
|
'insert': {
|
|
|
|
'commit': function( op ) {
|
|
|
|
this.insert( op );
|
|
|
|
},
|
|
|
|
'rollback': function( op ) {
|
|
|
|
this.remove( op );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Remove
|
|
|
|
'remove': {
|
|
|
|
'commit': function( op ) {
|
|
|
|
this.remove( op );
|
|
|
|
},
|
|
|
|
'rollback': function( op ) {
|
|
|
|
this.insert( op );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Change element attributes
|
|
|
|
'attribute': {
|
|
|
|
'commit': function( op ) {
|
|
|
|
this.attribute( op, false );
|
|
|
|
},
|
|
|
|
'rollback': function( op ) {
|
|
|
|
this.attribute( op, true );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Change content annotations
|
|
|
|
'annotate': {
|
|
|
|
'commit': function( op ) {
|
|
|
|
this.mark( op, false );
|
|
|
|
},
|
|
|
|
'rollback': function( op ) {
|
|
|
|
this.mark( op, true );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Static Methods */
|
|
|
|
|
|
|
|
es.TransactionProcessor.commit = function( doc, transaction ) {
|
|
|
|
var tp = new es.TransactionProcessor( doc, transaction );
|
|
|
|
tp.process( 'commit' );
|
|
|
|
};
|
|
|
|
|
|
|
|
es.TransactionProcessor.rollback = function( doc, transaction ) {
|
|
|
|
var tp = new es.TransactionProcessor( doc, transaction );
|
|
|
|
tp.process( 'rollback' );
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Methods */
|
|
|
|
|
|
|
|
es.TransactionProcessor.prototype.process = function( method ) {
|
|
|
|
var operations = this.transaction.getOperations();
|
|
|
|
for ( var i = 0, length = operations.length; i < length; i++ ) {
|
|
|
|
var operation = operations[i];
|
|
|
|
if ( operation.type in es.TransactionProcessor.operationMap ) {
|
|
|
|
es.TransactionProcessor.operationMap[operation.type][method].call( this, operation );
|
|
|
|
} else {
|
|
|
|
throw 'Invalid operation error. Operation type is not supported: ' + operation.type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-11-17 08:03:14 +00:00
|
|
|
es.TransactionProcessor.prototype.rebuildNodes = function( newData, oldNodes, parent, index ) {
|
2011-11-18 00:26:54 +00:00
|
|
|
var newNodes = es.DocumentModel.createNodesFromData( newData ),
|
|
|
|
remove = 0;
|
2011-11-17 08:03:14 +00:00
|
|
|
if ( oldNodes ) {
|
|
|
|
if ( oldNodes[0] === oldNodes[0].getRoot() ) {
|
|
|
|
parent = oldNodes[0];
|
|
|
|
index = 0;
|
|
|
|
remove = parent.getChildren().length;
|
|
|
|
} else {
|
|
|
|
parent = oldNodes[0].getParent();
|
|
|
|
index = parent.indexOf( oldNodes[0] );
|
|
|
|
remove = oldNodes.length;
|
|
|
|
}
|
2011-11-18 00:26:54 +00:00
|
|
|
// Try to preserve the first node
|
|
|
|
if (
|
|
|
|
// There must be an old and new node to preserve
|
|
|
|
newNodes.length &&
|
|
|
|
oldNodes.length &&
|
|
|
|
// Node types need to match
|
|
|
|
newNodes[0].type === oldNodes[0].type &&
|
|
|
|
// Only for leaf nodes
|
|
|
|
!newNodes[0].hasChildren()
|
|
|
|
) {
|
|
|
|
var newNode = newNodes.shift(),
|
|
|
|
oldNode = oldNodes.shift();
|
|
|
|
// Let's just leave this first node in place and adjust it's length
|
|
|
|
var newAttributes = newNode.getElement().attributes,
|
|
|
|
oldAttributes = oldNode.getElement().attributes;
|
|
|
|
if ( oldAttributes || newAttributes ) {
|
|
|
|
oldNode.getElement().attributes = newAttributes;
|
|
|
|
}
|
|
|
|
oldNode.adjustContentLength( newNode.getContentLength() - oldNode.getContentLength() );
|
|
|
|
index++;
|
|
|
|
remove--;
|
|
|
|
}
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
2011-11-17 18:11:48 +00:00
|
|
|
// Try to perform this in a single operation if possible, this reduces the number of UI updates
|
|
|
|
// TODO: Introduce a global for max argument length - 1024 is also assumed in es.insertIntoArray
|
|
|
|
if ( newNodes.length < 1024 ) {
|
|
|
|
parent.splice.apply( parent, [index, remove].concat( newNodes ) );
|
2011-11-18 00:26:54 +00:00
|
|
|
} else if ( newNodes.length ) {
|
2011-11-17 18:11:48 +00:00
|
|
|
parent.splice.apply( parent, [index, remove] );
|
|
|
|
// Safe to call with arbitrary length of newNodes
|
|
|
|
es.insertIntoArray( parent, index, newNodes );
|
|
|
|
}
|
2011-11-14 23:04:36 +00:00
|
|
|
};
|
|
|
|
|
2011-11-17 19:19:02 +00:00
|
|
|
/**
|
|
|
|
* Get the parent node that would be affected by inserting given data into it's child.
|
|
|
|
*
|
|
|
|
* This is used when inserting data that closes and reopens one or more parent nodes into a child
|
|
|
|
* node, which requires rebuilding at a higher level.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {es.DocumentNode} node Child node to start from
|
|
|
|
* @param {Array} data Data to inspect for closings
|
|
|
|
* @returns {es.DocumentNode} Lowest level parent node being affected
|
|
|
|
*/
|
2011-11-14 23:04:36 +00:00
|
|
|
es.TransactionProcessor.prototype.getScope = function( node, data ) {
|
|
|
|
var i,
|
|
|
|
length,
|
|
|
|
level = 0,
|
2011-11-17 19:19:02 +00:00
|
|
|
max = 0;
|
2011-11-14 23:04:36 +00:00
|
|
|
for ( i = 0, length = data.length; i < length; i++ ) {
|
|
|
|
if ( typeof data[i].type === 'string' ) {
|
2011-11-17 19:19:02 +00:00
|
|
|
level += data[i].type.charAt( 0 ) === '/' ? 1 : -1;
|
|
|
|
max = Math.max( max, level );
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
|
|
|
}
|
2011-11-17 19:19:02 +00:00
|
|
|
if ( max > 0 ) {
|
|
|
|
for ( i = 0; i < max - 1; i++ ) {
|
2011-11-14 23:04:36 +00:00
|
|
|
node = node.getParent();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return node;
|
|
|
|
};
|
|
|
|
|
|
|
|
es.TransactionProcessor.prototype.applyAnnotations = function( to ) {
|
|
|
|
var i,
|
|
|
|
j,
|
|
|
|
length,
|
|
|
|
annotation;
|
|
|
|
// Handle annotations
|
|
|
|
if ( this.set.length ) {
|
|
|
|
for ( i = 0, length = this.set.length; i < length; i++ ) {
|
|
|
|
annotation = this.set[i];
|
|
|
|
// Auto-build annotation hash
|
|
|
|
if ( annotation.hash === undefined ) {
|
|
|
|
annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
|
|
|
|
}
|
|
|
|
for ( j = this.cursor; j < to; j++ ) {
|
|
|
|
// Auto-convert to array
|
|
|
|
if ( es.isArray( this.model.data[j] ) ) {
|
|
|
|
this.model.data[j].push( annotation );
|
|
|
|
} else {
|
|
|
|
this.model.data[j] = [this.model.data[j], annotation];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( this.clear.length ) {
|
|
|
|
for ( i = 0, length = this.clear.length; i < length; i++ ) {
|
|
|
|
annotation = this.clear[i];
|
|
|
|
// Auto-build annotation hash
|
|
|
|
if ( annotation.hash === undefined ) {
|
|
|
|
annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
|
|
|
|
}
|
|
|
|
for ( j = this.cursor; j < to; j++ ) {
|
|
|
|
var index = es.DocumentModel.getIndexOfAnnotation( this.model.data[j], annotation );
|
|
|
|
if ( index !== -1 ) {
|
|
|
|
this.model.data[j].splice( index, 1 );
|
|
|
|
}
|
|
|
|
// Auto-convert to string
|
|
|
|
if ( this.model.data[j].length === 1 ) {
|
|
|
|
this.model.data[j] = this.model.data[j][0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
es.TransactionProcessor.prototype.retain = function( op ) {
|
|
|
|
this.applyAnnotations( this.cursor + op.length );
|
|
|
|
this.cursor += op.length;
|
|
|
|
};
|
|
|
|
|
|
|
|
es.TransactionProcessor.prototype.insert = function( op ) {
|
2011-11-17 08:03:14 +00:00
|
|
|
var node,
|
|
|
|
index,
|
|
|
|
offset;
|
2011-11-14 23:04:36 +00:00
|
|
|
if ( es.DocumentModel.isStructuralOffset( this.model.data, this.cursor ) ) {
|
2011-11-16 20:39:48 +00:00
|
|
|
es.insertIntoArray( this.model.data, this.cursor, op.data );
|
|
|
|
this.applyAnnotations( this.cursor + op.data.length );
|
2011-11-17 08:03:14 +00:00
|
|
|
node = this.model.getNodeFromOffset( this.cursor );
|
2011-11-17 18:05:14 +00:00
|
|
|
offset = this.model.getOffsetFromNode( node );
|
2011-11-17 18:16:02 +00:00
|
|
|
index = node.getIndexFromOffset( this.cursor - offset );
|
2011-11-17 08:03:14 +00:00
|
|
|
this.rebuildNodes( op.data, null, node, index );
|
2011-11-14 23:04:36 +00:00
|
|
|
} else {
|
2011-11-17 08:03:14 +00:00
|
|
|
node = this.model.getNodeFromOffset( this.cursor );
|
|
|
|
if ( node.getParent() === this.model ) {
|
|
|
|
offset = this.model.getOffsetFromNode( node );
|
|
|
|
index = this.model.getIndexFromOffset( this.cursor - offset );
|
|
|
|
} else {
|
|
|
|
node = this.getScope( node, op.data );
|
|
|
|
offset = this.model.getOffsetFromNode( node );
|
|
|
|
index = node.getIndexFromOffset( this.cursor - offset );
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
|
|
|
if ( es.DocumentModel.containsElementData( op.data ) ) {
|
|
|
|
// Perform insert on linear data model
|
|
|
|
es.insertIntoArray( this.model.data, this.cursor, op.data );
|
|
|
|
this.applyAnnotations( this.cursor + op.data.length );
|
|
|
|
// Synchronize model tree
|
|
|
|
if ( offset === -1 ) {
|
|
|
|
throw 'Invalid offset error. Node is not in model tree';
|
|
|
|
}
|
|
|
|
this.rebuildNodes(
|
|
|
|
this.model.data.slice( offset, offset + node.getElementLength() + op.data.length ),
|
|
|
|
[node]
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// Perform insert on linear data model
|
|
|
|
// TODO this is duplicated from above
|
|
|
|
es.insertIntoArray( this.model.data, this.cursor, op.data );
|
|
|
|
this.applyAnnotations( this.cursor + op.data.length );
|
|
|
|
// Update model tree
|
|
|
|
node.adjustContentLength( op.data.length, true );
|
|
|
|
node.emit( 'update', this.cursor - offset );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.cursor += op.data.length;
|
|
|
|
};
|
|
|
|
|
|
|
|
es.TransactionProcessor.prototype.remove = function( op ) {
|
|
|
|
if ( es.DocumentModel.containsElementData( op.data ) ) {
|
|
|
|
// Figure out which nodes are covered by the removal
|
|
|
|
var ranges = this.model.selectNodes( new es.Range( this.cursor, this.cursor + op.data.length ) );
|
|
|
|
var oldNodes = [], newData = [], firstKeptNode = true, lastElement;
|
|
|
|
for ( var i = 0; i < ranges.length; i++ ) {
|
|
|
|
oldNodes.push( ranges[i].node );
|
2011-11-16 20:01:12 +00:00
|
|
|
if ( ranges[i].range !== undefined ) {
|
2011-11-14 23:04:36 +00:00
|
|
|
// We have to keep part of this node
|
|
|
|
if ( firstKeptNode ) {
|
|
|
|
// This is the first node we're keeping
|
|
|
|
// Keep its opening as well
|
|
|
|
newData.push( ranges[i].node.getElement() );
|
|
|
|
firstKeptNode = false;
|
|
|
|
}
|
2011-11-14 23:47:07 +00:00
|
|
|
// Compute the start and end offset of this node
|
|
|
|
// We could do that with getOffsetFromNode() but
|
|
|
|
// we already have all the numbers we need so why would we
|
2011-11-14 23:04:36 +00:00
|
|
|
var startOffset = ranges[i].globalRange.start - ranges[i].range.start,
|
|
|
|
endOffset = startOffset + ranges[i].node.getContentLength(),
|
|
|
|
// Get this node's data
|
|
|
|
nodeData = this.model.data.slice( startOffset, endOffset );
|
|
|
|
// Remove data covered by the range from nodeData
|
|
|
|
nodeData.splice( ranges[i].range.start, ranges[i].range.end - ranges[i].range.start );
|
|
|
|
// What remains in nodeData is the data we need to keep
|
|
|
|
// Append it to newData
|
|
|
|
newData = newData.concat( nodeData );
|
|
|
|
|
|
|
|
lastElement = ranges[i].node.getElementType();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( lastElement !== undefined ) {
|
|
|
|
// Keep the closing of the last element that was partially kept
|
|
|
|
newData.push( { 'type': '/' + lastElement } );
|
|
|
|
}
|
2011-11-14 23:47:07 +00:00
|
|
|
// Update the linear model
|
|
|
|
this.model.data.splice( this.cursor, op.data.length );
|
2011-11-14 23:04:36 +00:00
|
|
|
// Perform the rebuild. This updates the model tree
|
|
|
|
this.rebuildNodes( newData, oldNodes );
|
|
|
|
} else {
|
|
|
|
// We're removing content only. Take a shortcut
|
|
|
|
// Get the node we are removing content from
|
|
|
|
var node = this.model.getNodeFromOffset( this.cursor );
|
|
|
|
// Update model tree
|
|
|
|
node.adjustContentLength( -op.data.length, true );
|
2011-11-14 23:47:07 +00:00
|
|
|
// Update the linear model
|
|
|
|
this.model.data.splice( this.cursor, op.data.length );
|
|
|
|
// Emit an update so things sync up
|
2011-11-17 18:54:52 +00:00
|
|
|
var offset = this.model.getOffsetFromNode( node );
|
|
|
|
node.emit( 'update', this.cursor - offset );
|
2011-11-14 23:04:36 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
es.TransactionProcessor.prototype.attribute = function( op, invert ) {
|
|
|
|
var element = this.model.data[this.cursor];
|
|
|
|
if ( element.type === undefined ) {
|
|
|
|
throw 'Invalid element error. Can not set attributes on non-element data.';
|
|
|
|
}
|
|
|
|
if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
|
|
|
|
// Automatically initialize attributes object
|
|
|
|
if ( !element.attributes ) {
|
|
|
|
element.attributes = {};
|
|
|
|
}
|
|
|
|
element.attributes[op.key] = op.value;
|
|
|
|
} else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
|
|
|
|
if ( element.attributes ) {
|
|
|
|
delete element.attributes[op.key];
|
|
|
|
}
|
|
|
|
// Automatically clean up attributes object
|
|
|
|
var empty = true;
|
|
|
|
for ( var key in element.attributes ) {
|
|
|
|
empty = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if ( empty ) {
|
|
|
|
delete element.attributes;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw 'Invalid method error. Can not operate attributes this way: ' + method;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
es.TransactionProcessor.prototype.mark = function( op, invert ) {
|
|
|
|
var target;
|
|
|
|
if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
|
|
|
|
target = this.set;
|
|
|
|
} else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
|
|
|
|
target = this.clear;
|
|
|
|
} else {
|
|
|
|
throw 'Invalid method error. Can not operate attributes this way: ' + method;
|
|
|
|
}
|
|
|
|
if ( op.bias === 'start' ) {
|
|
|
|
target.push( op.annotation );
|
|
|
|
} else if ( op.bias === 'stop' ) {
|
|
|
|
var index = es.DocumentModel.getIndexOfAnnotation( target, op.annotation );
|
|
|
|
if ( index === -1 ) {
|
|
|
|
throw 'Annotation stack error. Annotation is missing.';
|
|
|
|
}
|
|
|
|
target.splice( index, 1 );
|
|
|
|
}
|
|
|
|
};
|