2013-04-17 17:53:26 +00:00
|
|
|
/*!
|
|
|
|
* 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
|
2013-05-01 22:21:32 +00:00
|
|
|
* @mixins ve.EventEmitter
|
|
|
|
*
|
2013-04-17 17:53:26 +00:00
|
|
|
* @constructor
|
|
|
|
* @param {ve.dm.Document} doc Document model
|
|
|
|
*/
|
|
|
|
ve.dm.InternalList = function VeDmInternalList( doc ) {
|
2013-05-01 22:21:32 +00:00
|
|
|
// Mixin constructors
|
2013-04-17 17:53:26 +00:00
|
|
|
ve.EventEmitter.call( this );
|
|
|
|
|
|
|
|
// Properties
|
|
|
|
this.document = doc;
|
|
|
|
this.store = new ve.dm.IndexValueStore();
|
2013-05-29 14:46:52 +00:00
|
|
|
this.itemHtmlQueue = [];
|
2013-04-17 17:53:26 +00:00
|
|
|
this.listNode = null;
|
2013-05-29 14:46:52 +00:00
|
|
|
this.nodes = {};
|
2013-06-03 20:22:35 +00:00
|
|
|
this.groupsChanged = [];
|
2013-04-17 17:53:26 +00:00
|
|
|
|
|
|
|
// Event handlers
|
2013-06-03 20:22:35 +00:00
|
|
|
this.getDocument().connect( this, { 'transact': 'onTransact' } );
|
2013-04-17 17:53:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/* Inheritance */
|
|
|
|
|
2013-05-01 22:21:32 +00:00
|
|
|
ve.mixinClass( ve.dm.InternalList, ve.EventEmitter );
|
2013-04-17 17:53:26 +00:00
|
|
|
|
2013-06-03 20:22:35 +00:00
|
|
|
/* Events */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event update
|
|
|
|
* @param {string[]} groupsChanged List of groups changed since the last transaction
|
|
|
|
*/
|
|
|
|
|
2013-04-17 17:53:26 +00:00
|
|
|
/* Methods */
|
|
|
|
|
|
|
|
/**
|
2013-05-28 13:07:46 +00:00
|
|
|
* Queues up an item's html for parsing later.
|
2013-04-17 17:53:26 +00:00
|
|
|
*
|
2013-05-29 14:46:52 +00:00
|
|
|
* If an item with the specified group and key already exists it will be ignored.
|
2013-04-17 17:53:26 +00:00
|
|
|
*
|
|
|
|
* @method
|
2013-05-29 14:46:52 +00:00
|
|
|
* @param {string} groupName Item group
|
2013-04-17 17:53:26 +00:00
|
|
|
* @param {string} key Item key
|
2013-05-28 13:07:46 +00:00
|
|
|
* @param {string} html Item contents
|
2013-04-17 17:53:26 +00:00
|
|
|
* @returns {number} Index of the item in the index-value store, and also the list
|
|
|
|
*/
|
2013-05-29 14:46:52 +00:00
|
|
|
ve.dm.InternalList.prototype.queueItemHtml = function ( groupName, key, html ) {
|
|
|
|
var index = this.getStore().indexOfHash( groupName + '/' + key );
|
2013-04-17 17:53:26 +00:00
|
|
|
if ( index === null ) {
|
2013-05-29 14:46:52 +00:00
|
|
|
index = this.getStore().index( html, groupName + '/' + key );
|
|
|
|
this.itemHtmlQueue.push( index );
|
2013-04-17 17:53:26 +00:00
|
|
|
}
|
|
|
|
return index;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets all the item's HTML strings
|
|
|
|
* @method
|
|
|
|
* @returns {Object} Name-indexed object containing HTMLElements
|
|
|
|
*/
|
2013-05-29 14:46:52 +00:00
|
|
|
ve.dm.InternalList.prototype.getItemHtmlQueue = function () {
|
|
|
|
return this.getStore().values( this.itemHtmlQueue );
|
2013-04-17 17:53:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the internal list's document model
|
|
|
|
* @method
|
|
|
|
* @returns {ve.dm.Document} Document model
|
|
|
|
*/
|
|
|
|
ve.dm.InternalList.prototype.getDocument = function () {
|
|
|
|
return this.document;
|
|
|
|
};
|
|
|
|
|
2013-05-28 18:21:59 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
};
|
|
|
|
|
2013-04-17 17:53:26 +00:00
|
|
|
/**
|
|
|
|
* 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];
|
|
|
|
};
|
|
|
|
|
2013-06-03 20:22:35 +00:00
|
|
|
/**
|
|
|
|
* Get the node group object for a specified group name.
|
|
|
|
* @param {string} groupName Name of the group
|
|
|
|
* @returns {Object} Node group object, containing nodes and key order array
|
|
|
|
*/
|
|
|
|
ve.dm.InternalList.prototype.getNodeGroup = function ( groupName ) {
|
|
|
|
return this.nodes[groupName];
|
|
|
|
};
|
|
|
|
|
2013-04-17 17:53:26 +00:00
|
|
|
/**
|
2013-05-28 13:07:46 +00:00
|
|
|
* Converts stored item HTML into linear data.
|
2013-04-17 17:53:26 +00:00
|
|
|
*
|
|
|
|
* Each item is an InternalItem, and they are wrapped in an InternalList.
|
|
|
|
* If there are no items an empty array is returned.
|
|
|
|
*
|
2013-05-28 13:07:46 +00:00
|
|
|
* Stored HTML is deleted after conversion.
|
|
|
|
*
|
2013-04-17 17:53:26 +00:00
|
|
|
* @method
|
|
|
|
* @param {ve.dm.Converter} converter Converter object
|
|
|
|
* @returns {Array} Linear model data
|
|
|
|
*/
|
2013-05-28 13:07:46 +00:00
|
|
|
ve.dm.InternalList.prototype.convertToData = function ( converter ) {
|
2013-04-17 17:53:26 +00:00
|
|
|
var i, length, itemData,
|
2013-05-29 14:46:52 +00:00
|
|
|
itemHtmlQueue = this.getItemHtmlQueue(), list = [];
|
2013-04-17 17:53:26 +00:00
|
|
|
|
2013-05-29 14:46:52 +00:00
|
|
|
if ( itemHtmlQueue.length ) {
|
2013-04-17 17:53:26 +00:00
|
|
|
list.push( { 'type': 'internalList' } );
|
2013-05-29 14:46:52 +00:00
|
|
|
for ( i = 0, length = itemHtmlQueue.length; i < length; i++ ) {
|
|
|
|
itemData = converter.getDataFromDomRecursion( $( '<div>' ).html( itemHtmlQueue[i] )[0] );
|
2013-04-17 17:53:26 +00:00
|
|
|
list = list.concat(
|
|
|
|
[{ 'type': 'internalItem' }],
|
|
|
|
itemData,
|
|
|
|
[{ 'type': '/internalItem' }]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
list.push( { 'type': '/internalList' } );
|
|
|
|
}
|
2013-05-28 13:07:46 +00:00
|
|
|
// After conversion we no longer need the HTML
|
2013-05-29 14:46:52 +00:00
|
|
|
this.itemHtmlQueue = [];
|
2013-04-17 17:53:26 +00:00
|
|
|
return list;
|
2013-05-22 14:48:41 +00:00
|
|
|
};
|
|
|
|
|
2013-06-03 20:22:35 +00:00
|
|
|
/**
|
|
|
|
* Get position of a key within a group
|
|
|
|
* @param {string} groupName Name of the group
|
|
|
|
* @param {string} key Name of the key
|
|
|
|
* @returns {number} Position within the key ordering for that group
|
|
|
|
*/
|
|
|
|
ve.dm.InternalList.prototype.getKeyPosition = function ( groupName, key ) {
|
|
|
|
return ve.indexOf( key, this.nodes[groupName].keyOrder );
|
|
|
|
};
|
|
|
|
|
2013-05-29 14:46:52 +00:00
|
|
|
/**
|
|
|
|
* 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] = {
|
2013-06-03 20:22:35 +00:00
|
|
|
'keyNodes': {},
|
|
|
|
'keyOrder': []
|
2013-05-29 14:46:52 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
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 );
|
2013-06-03 20:22:35 +00:00
|
|
|
}
|
|
|
|
this.markGroupAsChanged( groupName );
|
|
|
|
};
|
2013-05-29 14:46:52 +00:00
|
|
|
|
2013-06-03 20:22:35 +00:00
|
|
|
/**
|
|
|
|
* Mark a node group as having been changed since the last transaction.
|
|
|
|
* @param {string} groupName Name of group which has changed
|
|
|
|
*/
|
|
|
|
ve.dm.InternalList.prototype.markGroupAsChanged = function ( groupName ) {
|
|
|
|
if ( ve.indexOf( groupName, this.groupsChanged ) === -1 ) {
|
|
|
|
this.groupsChanged.push( groupName );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle document transaction events
|
|
|
|
* @emits update
|
|
|
|
*/
|
|
|
|
ve.dm.InternalList.prototype.onTransact = function () {
|
|
|
|
var i;
|
|
|
|
if ( this.groupsChanged.length > 0 ) {
|
|
|
|
// length will almost always be 1, so probably better to not cache it
|
|
|
|
for ( i = 0; i < this.groupsChanged.length; i++ ) {
|
|
|
|
this.sortGroupKeys( this.nodes[this.groupsChanged[i]] );
|
|
|
|
}
|
|
|
|
this.emit( 'update', this.groupsChanged );
|
|
|
|
this.groupsChanged = [];
|
2013-05-29 14:46:52 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 );
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2013-06-03 20:22:35 +00:00
|
|
|
this.markGroupAsChanged( groupName );
|
2013-05-29 14:46:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
2013-05-22 14:48:41 +00:00
|
|
|
/**
|
|
|
|
* 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 ) {
|
2013-05-28 18:21:59 +00:00
|
|
|
var clone = new this.constructor( doc || this.getDocument() );
|
|
|
|
clone.store = this.getStore().clone();
|
2013-05-22 14:48:41 +00:00
|
|
|
return clone;
|
|
|
|
};
|
2013-05-22 14:58:21 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 ) {
|
2013-05-28 18:21:59 +00:00
|
|
|
var i, len, index, storeMapping = this.getStore().merge( other.getStore() ), mapping = {};
|
2013-05-29 14:46:52 +00:00
|
|
|
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 );
|
2013-05-22 14:58:21 +00:00
|
|
|
if ( index === -1 ) {
|
2013-05-29 14:46:52 +00:00
|
|
|
index = this.itemHtmlQueue.push( other.itemHtmlQueue[i] ) - 1;
|
2013-05-22 14:58:21 +00:00
|
|
|
}
|
|
|
|
mapping[i] = index;
|
|
|
|
}
|
|
|
|
return mapping;
|
|
|
|
};
|