mediawiki-extensions-Visual.../modules/ve/dm/ve.dm.InternalList.js
Ed Sanders 2925966944 Store DM nodes in InternalList
Also keep items in the order they appear in the document
and grouped by group and key.

Additions and removals are triggered by the new root/unroot events.

Change-Id: Ia3e90ccfdab88f352b89992b90554e5f03ff9952
2013-06-03 22:10:07 +01:00

266 lines
7.2 KiB
JavaScript

/*!
* VisualEditor DataModel InternalList 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.Document} doc Document model
*/
ve.dm.InternalList = function VeDmInternalList( doc ) {
// Mixin constructors
ve.EventEmitter.call( this );
// Properties
this.document = doc;
this.store = new ve.dm.IndexValueStore();
this.itemHtmlQueue = [];
this.listNode = null;
this.nodes = {};
// Event handlers
// this.getDocument().connect( this, { 'transact': 'onTransact' } );
};
/* Inheritance */
ve.mixinClass( ve.dm.InternalList, ve.EventEmitter );
/* Methods */
/**
* Queues up an item's html for parsing later.
*
* If an item with the specified group and key already exists it will be ignored.
*
* @method
* @param {string} groupName Item group
* @param {string} key Item key
* @param {string} html Item contents
* @returns {number} Index of the item in the index-value store, and also the list
*/
ve.dm.InternalList.prototype.queueItemHtml = function ( groupName, key, html ) {
var index = this.getStore().indexOfHash( groupName + '/' + key );
if ( index === null ) {
index = this.getStore().index( html, groupName + '/' + key );
this.itemHtmlQueue.push( index );
}
return index;
};
/**
* Gets all the item's HTML strings
* @method
* @returns {Object} Name-indexed object containing HTMLElements
*/
ve.dm.InternalList.prototype.getItemHtmlQueue = function () {
return this.getStore().values( this.itemHtmlQueue );
};
/**
* Gets the internal list's document model
* @method
* @returns {ve.dm.Document} Document model
*/
ve.dm.InternalList.prototype.getDocument = function () {
return this.document;
};
/**
* Gets the internal list's index value store
* @method
* @returns {ve.dm.IndexValueStore} Index value store
*/
ve.dm.InternalList.prototype.getStore = function () {
return this.store;
};
/**
* Get the list node
* @method
* @returns {ve.dm.InternalListNode} List node
*/
ve.dm.InternalList.prototype.getListNode = function () {
var i, nodes;
// find listNode if not set, or unattached
if ( !this.listNode || !this.listNode.doc ) {
nodes = this.getDocument().documentNode.children;
for ( i = nodes.length; i >= 0; i-- ) {
if ( nodes[i] instanceof ve.dm.InternalListNode ) {
this.listNode = nodes[i];
break;
}
}
}
return this.listNode;
};
/**
* Get the item node from a specific index
* @method
* @param {number} index Item's index
* @returns {ve.dm.InternalItemNode} Item node
*/
ve.dm.InternalList.prototype.getItemNode = function ( index ) {
return this.getListNode().children[index];
};
/**
* Converts stored item HTML into linear data.
*
* Each item is an InternalItem, and they are wrapped in an InternalList.
* If there are no items an empty array is returned.
*
* Stored HTML is deleted after conversion.
*
* @method
* @param {ve.dm.Converter} converter Converter object
* @returns {Array} Linear model data
*/
ve.dm.InternalList.prototype.convertToData = function ( converter ) {
var i, length, itemData,
itemHtmlQueue = this.getItemHtmlQueue(), list = [];
if ( itemHtmlQueue.length ) {
list.push( { 'type': 'internalList' } );
for ( i = 0, length = itemHtmlQueue.length; i < length; i++ ) {
itemData = converter.getDataFromDomRecursion( $( '<div>' ).html( itemHtmlQueue[i] )[0] );
list = list.concat(
[{ 'type': 'internalItem' }],
itemData,
[{ 'type': '/internalItem' }]
);
}
list.push( { 'type': '/internalList' } );
}
// After conversion we no longer need the HTML
this.itemHtmlQueue = [];
return list;
};
/**
* Add a node.
* @method
* @param {string} groupName Item group
* @param {string} key Item name
* @param {ve.dm.Node} node Item node
*/
ve.dm.InternalList.prototype.addNode = function ( groupName, key, node ) {
var i, len, start, keyNodes, group = this.nodes[groupName];
// The group may not exist yet
if ( group === undefined ) {
group = this.nodes[groupName] = {
keyNodes: {},
keyOrder: []
};
}
keyNodes = group.keyNodes[key];
// The key may not exist yet
if ( keyNodes === undefined ) {
keyNodes = group.keyNodes[key] = [];
}
if ( ve.indexOf( key, group.keyOrder ) === -1 ) {
group.keyOrder.push( key );
}
if ( node.getDocument().buildingNodeTree ) {
// If the document is building the original node tree
// then every item is being added in order, so we don't
// need to worry about sorting.
keyNodes.push( node );
} else {
// TODO: We could use binary search insertion sort
start = node.getRange().start;
for ( i = 0, len = keyNodes.length; i < len; i++ ) {
if ( start < keyNodes[i].getRange().start ) {
break;
}
}
// 'i' is now the insertion point, so add the node here
keyNodes.splice( i, 0, node );
this.sortGroupKeys( group );
}
};
/**
* Remove a node.
* @method
* @param {string} groupName Item group
* @param {string} key Item name
* @param {ve.dm.Node} node Item node
*/
ve.dm.InternalList.prototype.removeNode = function ( groupName, key, node ) {
var i, len, j,
group = this.nodes[groupName],
keyNodes = group.keyNodes[key];
for ( i = 0, len = keyNodes.length; i < len; i++ ) {
if ( keyNodes[i] === node ) {
keyNodes.splice( i, 1 );
if ( keyNodes.length === 0 ) {
delete group.keyNodes[key];
j = ve.indexOf( key, group.keyOrder );
group.keyOrder.splice( j, 1 );
} else {
// If the key has been emptied then removing it from the keyOrder
// array is sufficient to keep it ordered, otherwise we need to
// re-sort the keys, as we don't know the position of the new
// 0th item in the key
this.sortGroupKeys( group );
}
break;
}
}
};
/**
* Sort the keyOrder array within a group object.
* @param {Object} group Group object
*/
ve.dm.InternalList.prototype.sortGroupKeys = function ( group ) {
// Sort keyOrder
group.keyOrder.sort( function ( key1, key2 ) {
return group.keyNodes[key1][0].getRange().start - group.keyNodes[key2][0].getRange().start;
} );
};
/**
* Clone this internal list.
*
* @param {ve.dm.Document} [doc] The new list's document. Defaults to this list's document.
* @returns {ve.dm.InternalList} Clone of this internal
*/
ve.dm.InternalList.prototype.clone = function ( doc ) {
var clone = new this.constructor( doc || this.getDocument() );
clone.store = this.getStore().clone();
return clone;
};
/**
* Merge another document's internal list into this one.
*
* Objects that are in other but not in this are added to this, possibly with a different index.
*
* @param {ve.dm.InternalList} other List to merge into this one
* @returns {Object} Object in which the keys are indexes in other and the values are the corresponding keys in this
*/
ve.dm.InternalList.prototype.merge = function ( other ) {
var i, len, index, storeMapping = this.getStore().merge( other.getStore() ), mapping = {};
for ( i = 0, len = other.itemHtmlQueue.length; i < len; i++ ) {
other.itemHtmlQueue[i] = storeMapping[other.itemHtmlQueue[i]];
index = ve.indexOf( other.itemHtmlQueue[i], this.itemHtmlQueue );
if ( index === -1 ) {
index = this.itemHtmlQueue.push( other.itemHtmlQueue[i] ) - 1;
}
mapping[i] = index;
}
return mapping;
};