mediawiki-extensions-Visual.../modules/ve/dm/ve.dm.MetaList.js

335 lines
11 KiB
JavaScript
Raw Normal View History

/*!
* 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 calculate 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 to insert the new metadata, or undefined to add to the end
* @param {Number} [index] Index 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 ( offset === undefined ) {
offset = this.document.data.getLength();
}
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;
};