mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-12 09:09:25 +00:00
Merge changes I4b62a310,I7c9f22a1
* changes: Add mutators in MetaList Move .commit()/.rollback() from TransactionProcessor to Document
This commit is contained in:
commit
34cc39bf9f
|
@ -449,20 +449,32 @@ ve.dm.Document.getDataSlice = function ( sourceData, range, deep ) {
|
|||
* Reverse a transaction's effects on the content data.
|
||||
*
|
||||
* @method
|
||||
* @param {ve.dm.Transaction}
|
||||
* @param {ve.dm.Transaction} transaction Transaction to roll back
|
||||
* @emits transact
|
||||
* @throws {Error} Cannot roll back a transaction that has not been committed
|
||||
*/
|
||||
ve.dm.Document.prototype.rollback = function ( transaction ) {
|
||||
ve.dm.TransactionProcessor.rollback( this, transaction );
|
||||
if ( !transaction.hasBeenApplied() ) {
|
||||
throw new Error( 'Cannot roll back a transaction that has not been committed' );
|
||||
}
|
||||
new ve.dm.TransactionProcessor( this, transaction, true ).process();
|
||||
this.emit( 'transact', transaction, true );
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply a transaction's effects on the content data.
|
||||
*
|
||||
* @method
|
||||
* @param {ve.dm.Transaction}
|
||||
* @param {ve.dm.Transaction} transaction Transaction to apply
|
||||
* @emits transact
|
||||
* @throws {Error} Cannot commit a transaction that has already been committed
|
||||
*/
|
||||
ve.dm.Document.prototype.commit = function ( transaction ) {
|
||||
ve.dm.TransactionProcessor.commit( this, transaction );
|
||||
if ( transaction.hasBeenApplied() ) {
|
||||
throw new Error( 'Cannot commit a transaction that has already been committed' );
|
||||
}
|
||||
new ve.dm.TransactionProcessor( this, transaction, false ).process();
|
||||
this.emit( 'transact', transaction, false );
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -162,6 +162,17 @@ ve.dm.MetaItem.static.storeHtmlAttributes = true;
|
|||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Remove this item from the document. Only works if the item is attached to a MetaList.
|
||||
* @throws {Error} Cannot remove detached item
|
||||
*/
|
||||
ve.dm.MetaItem.prototype.remove = function () {
|
||||
if ( !this.list ) {
|
||||
throw new Error( 'Cannot remove detached item' );
|
||||
}
|
||||
this.list.removeMeta( this );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the group this meta item belongs to.
|
||||
* @see ve.dm.MetaItem#static.group
|
||||
|
|
|
@ -11,15 +11,16 @@
|
|||
* @class
|
||||
* @extends ve.EventEmitter
|
||||
* @constructor
|
||||
* @param {ve.dm.Document} doc Document
|
||||
* @param {ve.dm.Surface} surface Surface model
|
||||
*/
|
||||
ve.dm.MetaList = function VeDmMetaList( doc ) {
|
||||
ve.dm.MetaList = function VeDmMetaList( surface ) {
|
||||
var i, j, jlen, metadata, item, group;
|
||||
// Parent constructor
|
||||
ve.EventEmitter.call( this );
|
||||
|
||||
// Properties
|
||||
this.document = doc;
|
||||
this.surface = surface;
|
||||
this.document = surface.getDocument();
|
||||
this.groups = {};
|
||||
this.items = [];
|
||||
|
||||
|
@ -84,12 +85,12 @@ ve.dm.MetaList.prototype.onTransact = function ( tx, reversed ) {
|
|||
ins = reversed ? ops[i].remove : ops[i].insert;
|
||||
rm = reversed ? ops[i].insert : ops[i].remove;
|
||||
for ( j = 0, jlen = rm.length; j < jlen; j++ ) {
|
||||
this.removeItem( offset, index + j );
|
||||
this.deleteRemovedItem( offset, index + j );
|
||||
}
|
||||
for ( j = 0, jlen = ins.length; j < jlen; j++ ) {
|
||||
item = ve.dm.metaItemFactory.createFromElement( ins[j] );
|
||||
// offset and index are pre-transaction, but we'll fix them later
|
||||
this.insertItem( offset, index + j, item );
|
||||
this.addInsertedItem( offset, index + j, item );
|
||||
}
|
||||
index += rm.length;
|
||||
break;
|
||||
|
@ -184,18 +185,51 @@ ve.dm.MetaList.prototype.getAllItems = function () {
|
|||
return this.items.slice( 0 );
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert new metadata into the document. This builds and processes a transaction that inserts
|
||||
* metadata into the document.
|
||||
* @param {Object|ve.dm.MetaItem} meta Metadata element (or MetaItem) to insert
|
||||
* @param {Number} offset Offset at which to insert the new metadata
|
||||
* @param {Number} [index] Index at which to insert the new metadata, or undefined to add to the end
|
||||
*/
|
||||
ve.dm.MetaList.prototype.insertMeta = function ( meta, offset, index ) {
|
||||
var tx;
|
||||
if ( meta instanceof ve.dm.MetaItem ) {
|
||||
meta = meta.getElement();
|
||||
}
|
||||
if ( index === undefined ) {
|
||||
index = ( this.document.metadata[offset] || [] ).length;
|
||||
}
|
||||
tx = ve.dm.Transaction.newFromMetadataInsertion( this.document, offset, index, [ meta ] );
|
||||
this.surface.change( tx );
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a meta item from the document. This builds and processes a transaction that removes the
|
||||
* associated metadata from the document.
|
||||
* @param {ve.dm.MetaItem} item Item to remove
|
||||
*/
|
||||
ve.dm.MetaList.prototype.removeMeta = function ( item ) {
|
||||
var tx;
|
||||
tx = ve.dm.Transaction.newFromMetadataRemoval(
|
||||
this.document,
|
||||
item.getOffset(),
|
||||
new ve.Range( item.getIndex(), item.getIndex() + 1 )
|
||||
);
|
||||
this.surface.change( tx );
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert an item at a given offset and index in response to a transaction.
|
||||
*
|
||||
* This function is for internal usage by onTransact(). To actually insert an item, you need to
|
||||
* process a transaction against the document that inserts metadata, then the MetaList will
|
||||
* automatically update itself and add the item.
|
||||
* This function is for internal usage by onTransact(). To actually insert an item, use
|
||||
* insertItem().
|
||||
*
|
||||
* @param {number} offset Offset in the linear model of the new item
|
||||
* @param {number} index Index of the new item in the metadata array at offset
|
||||
* @param {ve.dm.MetaItem} item Item object
|
||||
*/
|
||||
ve.dm.MetaList.prototype.insertItem = function ( offset, index, item ) {
|
||||
ve.dm.MetaList.prototype.addInsertedItem = function ( offset, index, item ) {
|
||||
var group = item.getGroup(), at = this.findItem( offset, index, null, true );
|
||||
this.items.splice( at, 0, item );
|
||||
if ( this.groups[group] ) {
|
||||
|
@ -210,14 +244,13 @@ ve.dm.MetaList.prototype.insertItem = function ( offset, index, item ) {
|
|||
/**
|
||||
* Remove an item in response to a transaction.
|
||||
*
|
||||
* This function is for internal usage by onTransact(). To actually remove an item, you need to
|
||||
* process a transaction against the document that removes the associated metadata, then the
|
||||
* MetaList will automatically update itself and remove the item.
|
||||
* This function is for internal usage by onTransact(). To actually remove an item, use
|
||||
* removeItem().
|
||||
*
|
||||
* @param {number} offset Offset in the linear model of the item
|
||||
* @param {number} index Index of the item in the metadata array at offset
|
||||
*/
|
||||
ve.dm.MetaList.prototype.removeItem = function ( offset, index ) {
|
||||
ve.dm.MetaList.prototype.deleteRemovedItem = function ( offset, index ) {
|
||||
var item, group, at = this.findItem( offset, index );
|
||||
if ( at === null ) {
|
||||
return;
|
||||
|
|
|
@ -285,7 +285,7 @@ ve.dm.Surface.prototype.change = function ( transactions, selection ) {
|
|||
this.bigStack = this.bigStack.slice( 0, this.bigStack.length - this.undoIndex );
|
||||
this.undoIndex = 0;
|
||||
this.smallStack.push( transactions[i] );
|
||||
ve.dm.TransactionProcessor.commit( this.documentModel, transactions[i] );
|
||||
this.documentModel.commit( transactions[i] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
* DataModel transaction processor.
|
||||
*
|
||||
* This class reads operations from a transaction and applies them one by one. It's not intended
|
||||
* to be used directly; use the static functions ve.dm.TransactionProcessor.commit() and .rollback()
|
||||
* instead.
|
||||
* to be used directly; use the .commit() and .rollback() methods of ve.dm.Document.
|
||||
*
|
||||
* NOTE: Instances of this class are not recyclable: you can only call .process() on them once.
|
||||
*
|
||||
|
@ -42,40 +41,6 @@ ve.dm.TransactionProcessor = function VeDmTransactionProcessor( doc, transaction
|
|||
/* See ve.dm.TransactionProcessor.processors */
|
||||
ve.dm.TransactionProcessor.processors = {};
|
||||
|
||||
/* Static methods */
|
||||
|
||||
/**
|
||||
* Commit a transaction to a document.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
* @param {ve.dm.Document} doc Document object to apply the transaction to
|
||||
* @param {ve.dm.Transaction} transaction Transaction to apply
|
||||
*/
|
||||
ve.dm.TransactionProcessor.commit = function ( doc, transaction ) {
|
||||
if ( transaction.hasBeenApplied() ) {
|
||||
throw new Error( 'Cannot commit a transaction that has already been committed' );
|
||||
}
|
||||
new ve.dm.TransactionProcessor( doc, transaction, false ).process();
|
||||
};
|
||||
|
||||
/**
|
||||
* Roll back a transaction.
|
||||
*
|
||||
* This applies the transaction to the document in reverse.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
* @param {ve.dm.Document} doc Document object to apply the transaction to
|
||||
* @param {ve.dm.Transaction} transaction Transaction to apply
|
||||
*/
|
||||
ve.dm.TransactionProcessor.rollback = function ( doc, transaction ) {
|
||||
if ( !transaction.hasBeenApplied() ) {
|
||||
throw new Error( 'Cannot roll back a transaction that has not been committed' );
|
||||
}
|
||||
new ve.dm.TransactionProcessor( doc, transaction, true ).process();
|
||||
};
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
|
@ -144,8 +109,6 @@ ve.dm.TransactionProcessor.prototype.process = function () {
|
|||
}
|
||||
// Mark the transaction as committed or rolled back, as appropriate
|
||||
this.transaction.toggleApplied();
|
||||
// Emit an event on the document
|
||||
this.document.emit( 'transact', this.transaction, this.reversed );
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1532,4 +1532,34 @@ QUnit.test( 'getSlice', function ( assert ) {
|
|||
cases[i].msg
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( 'protection against double application of transactions', 3, function ( assert ) {
|
||||
var tx = new ve.dm.Transaction(),
|
||||
testDocument = new ve.dm.Document( ve.dm.example.data );
|
||||
tx.pushRetain( 1 );
|
||||
tx.pushReplace( [], ['H', 'e', 'l', 'l', 'o' ] );
|
||||
assert.throws(
|
||||
function () {
|
||||
testDocument.rollback( tx );
|
||||
},
|
||||
Error,
|
||||
'exception thrown when trying to rollback an uncommitted transaction'
|
||||
);
|
||||
testDocument.commit( tx );
|
||||
assert.throws(
|
||||
function () {
|
||||
testDocument.commit( tx );
|
||||
},
|
||||
Error,
|
||||
'exception thrown when trying to commit an already-committed transaction'
|
||||
);
|
||||
testDocument.rollback( tx );
|
||||
assert.throws(
|
||||
function () {
|
||||
testDocument.rollback( tx );
|
||||
},
|
||||
Error,
|
||||
'exception thrown when trying to roll back a transaction that has already been rolled back'
|
||||
);
|
||||
} );
|
|
@ -30,7 +30,8 @@ function assertItemsMatchMetadata( assert, metadata, list, msg, full ) {
|
|||
QUnit.test( 'constructor', function ( assert ) {
|
||||
var i, n = 0,
|
||||
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
|
||||
list = new ve.dm.MetaList( doc ),
|
||||
surface = new ve.dm.Surface( doc ),
|
||||
list = new ve.dm.MetaList( surface ),
|
||||
metadata = doc.metadata;
|
||||
for ( i in metadata ) {
|
||||
if ( ve.isArray( metadata[i] ) ) {
|
||||
|
@ -42,7 +43,7 @@ QUnit.test( 'constructor', function ( assert ) {
|
|||
} );
|
||||
|
||||
QUnit.test( 'onTransact', function ( assert ) {
|
||||
var i, j, tx, list, n = 0,
|
||||
var i, j, surface, tx, list, n = 0,
|
||||
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
|
||||
comment = { 'type': 'alienMeta', 'attributes': { 'style': 'comment', 'text': 'onTransact test' } },
|
||||
heading = { 'type': 'heading', 'attributes': { 'level': 2 } },
|
||||
|
@ -134,10 +135,12 @@ QUnit.test( 'onTransact', function ( assert ) {
|
|||
tx[cases[i].calls[j][0]].apply( tx, cases[i].calls[j].slice( 1 ) );
|
||||
}
|
||||
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) );
|
||||
list = new ve.dm.MetaList( doc );
|
||||
ve.dm.TransactionProcessor.commit( doc, tx );
|
||||
surface = new ve.dm.Surface( doc );
|
||||
list = new ve.dm.MetaList( surface );
|
||||
// Test both the transaction-via-surface and transaction-via-document flows
|
||||
surface.change( tx );
|
||||
assertItemsMatchMetadata( assert, doc.metadata, list, cases[i].msg, false );
|
||||
ve.dm.TransactionProcessor.rollback( doc, tx );
|
||||
doc.rollback( tx );
|
||||
assertItemsMatchMetadata( assert, doc.metadata, list, cases[i].msg + ' (rollback)', false );
|
||||
}
|
||||
} );
|
||||
|
@ -147,8 +150,9 @@ QUnit.test( 'findItem', function ( assert ) {
|
|||
n = 0,
|
||||
groups = [ null ],
|
||||
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
|
||||
surface = new ve.dm.Surface( doc ),
|
||||
metadata = doc.metadata,
|
||||
list = new ve.dm.MetaList( doc );
|
||||
list = new ve.dm.MetaList( surface );
|
||||
|
||||
for ( i = 0; i < metadata.length; i++ ) {
|
||||
if ( ve.isArray( metadata[i] ) ) {
|
||||
|
@ -180,4 +184,60 @@ QUnit.test( 'findItem', function ( assert ) {
|
|||
assert.strictEqual( list.findItem( i, j, groups[g], true ), item + 1, groupDesc + ' (forInsertion) (' + i + ', ' + j + ')' );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( 'insertMeta', 5, function ( assert ) {
|
||||
var expected,
|
||||
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
|
||||
surface = new ve.dm.Surface( doc ),
|
||||
list = new ve.dm.MetaList( surface ),
|
||||
insert = {
|
||||
'type': 'alienMeta',
|
||||
'attributes': {
|
||||
'style': 'comment',
|
||||
'text': 'insertMeta test'
|
||||
}
|
||||
};
|
||||
|
||||
list.insertMeta( insert, 2, 0 );
|
||||
assert.deepEqual( doc.metadata[2], [ insert ], 'Inserting metadata at an offset without pre-existing metadata' );
|
||||
|
||||
expected = doc.metadata[0].slice( 0 );
|
||||
expected.splice( 1, 0, insert );
|
||||
list.insertMeta( insert, 0, 1 );
|
||||
assert.deepEqual( doc.metadata[0], expected, 'Inserting metadata in the middle' );
|
||||
|
||||
expected.push( insert );
|
||||
list.insertMeta( insert, 0 );
|
||||
assert.deepEqual( doc.metadata[0], expected, 'Inserting metadata without passing an index adds to the end' );
|
||||
|
||||
list.insertMeta( insert, 1 );
|
||||
assert.deepEqual( doc.metadata[1], [ insert ], 'Inserting metadata without passing an index without pre-existing metadata' );
|
||||
|
||||
list.insertMeta( new ve.dm.AlienMetaItem( insert ), 1 );
|
||||
assert.deepEqual( doc.metadata[1], [ insert, insert ], 'Passing a MetaItem rather than an element' );
|
||||
} );
|
||||
|
||||
QUnit.test( 'removeMeta', 4, function ( assert ) {
|
||||
var expected,
|
||||
doc = new ve.dm.Document( ve.copyArray( ve.dm.example.withMeta ) ),
|
||||
surface = new ve.dm.Surface( doc ),
|
||||
list = new ve.dm.MetaList( surface );
|
||||
|
||||
list.removeMeta( list.getItemAt( 4, 0 ) );
|
||||
assert.deepEqual( doc.metadata[4], [], 'Removing the only item at offset 4' );
|
||||
|
||||
expected = doc.metadata[0].slice( 0 );
|
||||
expected.splice( 1, 1 );
|
||||
list.removeMeta( list.getItemAt( 0, 1 ) );
|
||||
assert.deepEqual( doc.metadata[0], expected, 'Removing the item at (0,1)' );
|
||||
|
||||
expected = doc.metadata[11].slice( 0 );
|
||||
expected.splice( 0, 1 );
|
||||
list.getItemAt( 11, 0 ).remove();
|
||||
assert.deepEqual( doc.metadata[11], expected, 'Removing (11,0) using .remove()' );
|
||||
|
||||
expected.splice( 1, 1 );
|
||||
list.getItemAt( 11, 1 ).remove();
|
||||
assert.deepEqual( doc.metadata[11], expected, 'Removing (11,1) (formerly (11,2)) using .remove()' );
|
||||
} );
|
|
@ -9,37 +9,6 @@ QUnit.module( 've.dm.TransactionProcessor' );
|
|||
|
||||
/* Tests */
|
||||
|
||||
QUnit.test( 'protection against double application', 3, function ( assert ) {
|
||||
var tx,
|
||||
testDocument = new ve.dm.Document( ve.dm.example.data );
|
||||
tx = new ve.dm.Transaction();
|
||||
tx.pushRetain( 1 );
|
||||
tx.pushReplace( [], ['H', 'e', 'l', 'l', 'o' ] );
|
||||
assert.throws(
|
||||
function () {
|
||||
ve.dm.TransactionProcessor.rollback( testDocument, tx );
|
||||
},
|
||||
Error,
|
||||
'exception thrown when trying to rollback an uncommitted transaction'
|
||||
);
|
||||
ve.dm.TransactionProcessor.commit( testDocument, tx );
|
||||
assert.throws(
|
||||
function () {
|
||||
ve.dm.TransactionProcessor.commit( testDocument, tx );
|
||||
},
|
||||
Error,
|
||||
'exception thrown when trying to commit an already-committed transaction'
|
||||
);
|
||||
ve.dm.TransactionProcessor.rollback( testDocument, tx );
|
||||
assert.throws(
|
||||
function () {
|
||||
ve.dm.TransactionProcessor.rollback( testDocument, tx );
|
||||
},
|
||||
Error,
|
||||
'exception thrown when trying to roll back a transaction that has already been rolled back'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'commit/rollback', 86, function ( assert ) {
|
||||
var i, key, originalData, originalDoc, msg, testDocument, tx,
|
||||
expectedData, expectedDocument,
|
||||
|
@ -396,7 +365,7 @@ QUnit.test( 'commit/rollback', 86, function ( assert ) {
|
|||
cases[msg].expected( expectedData );
|
||||
expectedDocument = new ve.dm.Document( ve.copyArray ( expectedData ) );
|
||||
// Commit
|
||||
ve.dm.TransactionProcessor.commit( testDocument, tx );
|
||||
testDocument.commit( tx );
|
||||
assert.deepEqual( testDocument.getFullData(), expectedData, 'commit (data): ' + msg );
|
||||
assert.equalNodeTree(
|
||||
testDocument.getDocumentNode(),
|
||||
|
@ -404,7 +373,7 @@ QUnit.test( 'commit/rollback', 86, function ( assert ) {
|
|||
'commit (tree): ' + msg
|
||||
);
|
||||
// Rollback
|
||||
ve.dm.TransactionProcessor.rollback( testDocument, tx );
|
||||
testDocument.rollback( tx );
|
||||
assert.deepEqual( testDocument.getFullData(), originalData, 'rollback (data): ' + msg );
|
||||
assert.equalNodeTree(
|
||||
testDocument.getDocumentNode(),
|
||||
|
@ -415,7 +384,7 @@ QUnit.test( 'commit/rollback', 86, function ( assert ) {
|
|||
/*jshint loopfunc:true */
|
||||
assert.throws(
|
||||
function () {
|
||||
ve.dm.TransactionProcessor.commit( testDocument, tx );
|
||||
testDocument.commit( tx );
|
||||
},
|
||||
cases[msg].exception,
|
||||
'commit: ' + msg
|
||||
|
|
Loading…
Reference in a new issue