mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-25 23:05:35 +00:00
7233ea8f1b
The EventEmitter API we inherited from Node.js and then bastardized was getting awkward and cumbersome. The number of uses of ve.bind was getting out of control, and removing events meant caching the bound method in a property. Many of the "features" of EventEmitter wasn't even being used, some causing overhead, others just causing bloat. This change cleans up how EventEmitter is used throughout the codebase. The new event emitter API includes: * emit - identical to the previous API, no longer throws an error if you emit error without a handler * once - identical to the previous API, still introduces a wrapper* on - compatible with the previous API but has some new features * off - identical to removeListener in the previous API * connect - very similar to addListenerMethods but doesn't wrap callbacks in closures anymore * disconnect - new, basically the opposite of addListenerMethods Another change that is made in this commit is mixing in rather than inheriting from EventEmitter. Finally, there are changes throughout the codebase anywhere connect/disconnect could be used. Change-Id: Ic3085d39172a8a719ce7f036690f673e59848d3a
332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
/*!
|
|
* VisualEditor DataModel MetaList class.
|
|
*
|
|
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* DataModel meta item.
|
|
*
|
|
* @class
|
|
* @mixins ve.EventEmitter
|
|
*
|
|
* @constructor
|
|
* @param {ve.dm.Surface} surface Surface model
|
|
*/
|
|
ve.dm.MetaList = function VeDmMetaList( surface ) {
|
|
var i, j, jlen, metadata, item, group;
|
|
|
|
// Mixin constructors
|
|
ve.EventEmitter.call( this );
|
|
|
|
// Properties
|
|
this.surface = surface;
|
|
this.document = surface.getDocument();
|
|
this.groups = {};
|
|
this.items = [];
|
|
|
|
// Event handlers
|
|
this.document.connect( this, { 'transact': 'onTransact' } );
|
|
|
|
// Populate from document
|
|
metadata = this.document.getMetadata();
|
|
for ( i in metadata ) {
|
|
if ( metadata.hasOwnProperty( i ) && ve.isArray( metadata[i] ) ) {
|
|
for ( j = 0, jlen = metadata[i].length; j < jlen; j++ ) {
|
|
item = ve.dm.metaItemFactory.createFromElement( metadata[i][j] );
|
|
group = this.groups[item.getGroup()];
|
|
if ( !group ) {
|
|
group = this.groups[item.getGroup()] = [];
|
|
}
|
|
item.attach( this, Number( i ), j );
|
|
group.push( item );
|
|
this.items.push( item );
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
ve.mixinClass( ve.dm.MetaList, ve.EventEmitter );
|
|
|
|
/* Events */
|
|
|
|
/**
|
|
* @event insert
|
|
* @param {ve.dm.MetaItem} item Item that was inserted
|
|
*/
|
|
|
|
/**
|
|
* @event remove
|
|
* @param {ve.dm.MetaItem} item Item that was removed
|
|
* @param {Number} offset Linear model offset that the item was at
|
|
* @param {Number} index Index within that offset the item was at
|
|
*/
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Event handler for transactions on the document.
|
|
*
|
|
* When a transaction occurs, update this list to account for it:
|
|
* - insert items for new metadata that was inserted
|
|
* - remove items for metadata that was removed
|
|
* - translate offsets and recompute indices for metadata that has shifted
|
|
* @param {ve.dm.Transaction} tx Transaction that was applied to the document
|
|
* @param {boolean} reversed Whether the transaction was applied in reverse
|
|
* @emits insert
|
|
* @emits remove
|
|
*/
|
|
ve.dm.MetaList.prototype.onTransact = function ( tx, reversed ) {
|
|
var i, j, k, ilen, jlen, klen, ins, rm, retain, itemIndex, item,
|
|
offset = 0, newOffset = 0, index = 0, ops = tx.getOperations(),
|
|
insertedItems = [], removedItems = [];
|
|
// Look for replaceMetadata operations in the transaction and insert/remove items as appropriate
|
|
// This requires we also inspect retain, replace and replaceMetadata operations in order to
|
|
// track the offset and index. We track the pre-transaction offset, we need to do that in
|
|
// order to remove items correctly. This also means inserted items are initially at the wrong
|
|
// offset, but we translate it later.
|
|
for ( i = 0, ilen = ops.length; i < ilen; i++ ) {
|
|
switch ( ops[i].type ) {
|
|
case 'retain':
|
|
offset += ops[i].length;
|
|
newOffset += ops[i].length;
|
|
index = 0;
|
|
break;
|
|
case 'replace':
|
|
// if we have metadata replace info we can calulcate the new
|
|
// offset and index directly
|
|
ins = reversed ? ops[i].removeMetadata : ops[i].insertMetadata;
|
|
rm = reversed ? ops[i].insertMetadata : ops[i].removeMetadata;
|
|
retain = ops[i].retainMetadata || 0;
|
|
if ( rm !== undefined ) {
|
|
// find the first itemIndex - the rest should be in order after it
|
|
for ( j = 0, jlen = rm.length; j < jlen; j++ ) {
|
|
if ( rm[j] !== undefined ) {
|
|
itemIndex = this.findItem( offset + retain + j, 0 );
|
|
break;
|
|
}
|
|
}
|
|
// iterate through all the inserted metaItems
|
|
for ( j = 0, jlen = ins.length; j < jlen; j++ ) {
|
|
item = ins[j];
|
|
if ( item !== undefined ) {
|
|
for ( k = 0, klen = item.length; k < klen; k++ ) {
|
|
// Queue up the move for later so we don't break the metaItem ordering
|
|
this.items[itemIndex].setMove( newOffset + retain + j, k );
|
|
itemIndex++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
offset += reversed ? ops[i].insert.length : ops[i].remove.length;
|
|
newOffset += reversed ? ops[i].remove.length : ops[i].insert.length;
|
|
index = 0;
|
|
break;
|
|
case 'retainMetadata':
|
|
index += ops[i].length;
|
|
break;
|
|
case 'replaceMetadata':
|
|
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++ ) {
|
|
item = this.deleteRemovedItem( offset, index + j );
|
|
removedItems.push( { 'item': item, 'offset': offset, 'index': index } );
|
|
}
|
|
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.addInsertedItem( offset, index + j, item );
|
|
insertedItems.push( { 'item': item } );
|
|
}
|
|
index += rm.length;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Translate the offsets of all items, and reindex them too
|
|
// Reindexing is simple because the above ensures the items are already in the right order
|
|
offset = -1;
|
|
index = 0;
|
|
for ( i = 0, ilen = this.items.length; i < ilen; i++ ) {
|
|
if ( this.items[i].isMovePending() ) {
|
|
// move was calculated from metadata replace info, just apply it
|
|
this.items[i].applyMove();
|
|
} else {
|
|
// otherwise calculate the new offset from the transaction
|
|
this.items[i].setOffset( tx.translateOffset( this.items[i].getOffset(), reversed ) );
|
|
if ( this.items[i].getOffset() === offset ) {
|
|
index++;
|
|
} else {
|
|
index = 0;
|
|
}
|
|
this.items[i].setIndex( index );
|
|
}
|
|
offset = this.items[i].getOffset();
|
|
}
|
|
|
|
for ( i = 0, ilen = insertedItems.length; i < ilen; i++ ) {
|
|
this.emit( 'insert', insertedItems[i].item );
|
|
}
|
|
for ( i = 0, ilen = removedItems.length; i < ilen; i++ ) {
|
|
this.emit( 'remove', removedItems[i].item, removedItems[i].offset, removedItems[i].index );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Find an item by its offset, index and group.
|
|
*
|
|
* This function is mostly for internal usage.
|
|
*
|
|
* @param {number} offset Offset in the linear model
|
|
* @param {number} index Index in the metadata array associated with that offset
|
|
* @param {string} [group] Group to search in. If not set, search in all groups
|
|
* @param {boolean} [forInsertion] If the item is not found, return the index where it should have
|
|
* been rather than null
|
|
* @returns {number|null} Index into this.items or this.groups[group] where the item was found, or
|
|
* null if not found
|
|
*/
|
|
ve.dm.MetaList.prototype.findItem = function ( offset, index, group, forInsertion ) {
|
|
// Binary search for the item
|
|
var mid, items = typeof group === 'string' ? ( this.groups[group] || [] ) : this.items,
|
|
left = 0, right = items.length;
|
|
while ( left < right ) {
|
|
// Equivalent to Math.floor( ( left + right ) / 2 ) but much faster in V8
|
|
/*jshint bitwise:false */
|
|
mid = ( left + right ) >> 1;
|
|
if ( items[mid].getOffset() === offset && items[mid].getIndex() === index ) {
|
|
return mid;
|
|
}
|
|
if ( items[mid].getOffset() < offset || (
|
|
items[mid].getOffset() === offset && items[mid].getIndex() < index
|
|
) ) {
|
|
left = mid + 1;
|
|
} else {
|
|
right = mid;
|
|
}
|
|
}
|
|
return forInsertion ? left : null;
|
|
};
|
|
|
|
/**
|
|
* Get the item at a given offset and index, if there is one.
|
|
*
|
|
* @param {number} offset Offset in the linear model
|
|
* @param {number} index Index in the metadata array
|
|
* @returns {ve.dm.MetaItem|null} The item at (offset,index), or null if not found
|
|
*/
|
|
ve.dm.MetaList.prototype.getItemAt = function ( offset, index ) {
|
|
var at = this.findItem( offset, index );
|
|
return at === null ? null : this.items[at];
|
|
};
|
|
|
|
/**
|
|
* Get all items in a group.
|
|
*
|
|
* This function returns a shallow copy, so the array isn't returned by reference but the items
|
|
* themselves are.
|
|
*
|
|
* @param {string} group Group
|
|
* @returns {ve.dm.MetaItem[]} Array of items in the group (shallow copy)
|
|
*/
|
|
ve.dm.MetaList.prototype.getItemsInGroup = function ( group ) {
|
|
return ( this.groups[group] || [] ).slice( 0 );
|
|
};
|
|
|
|
/**
|
|
* Get all items in the list.
|
|
*
|
|
* This function returns a shallow copy, so the array isn't returned by reference but the items
|
|
* themselves are.
|
|
*
|
|
* @returns {ve.dm.MetaItem[]} Array of items in the list
|
|
*/
|
|
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.getData( 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, 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
|
|
* @emits insert
|
|
*/
|
|
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] ) {
|
|
at = this.findItem( offset, index, group, true );
|
|
this.groups[group].splice( at, 0, item );
|
|
} else {
|
|
this.groups[group] = [ item ];
|
|
}
|
|
item.attach( this, offset, index );
|
|
};
|
|
|
|
/**
|
|
* Remove an item in response to a transaction.
|
|
*
|
|
* 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
|
|
* @emits remove
|
|
*/
|
|
ve.dm.MetaList.prototype.deleteRemovedItem = function ( offset, index ) {
|
|
var item, group, at = this.findItem( offset, index );
|
|
if ( at === null ) {
|
|
return;
|
|
}
|
|
item = this.items[at];
|
|
group = item.getGroup();
|
|
this.items.splice( at, 1 );
|
|
at = this.findItem( offset, index, group );
|
|
if ( at !== null ) {
|
|
this.groups[group].splice( at, 1 );
|
|
}
|
|
item.detach( this );
|
|
return item;
|
|
};
|