mediawiki-extensions-Visual.../modules/ve/dm/ve.dm.AnnotationSet.js
Trevor Parscal c6e0eee837 Configurable insertion annotations
This makes it possible to use a static property to configure whether an
annotation should be applied to content added after it. This makes it
possible to do this for normal style stuff, but not for links.

TODO: Inez is going to add IE support for this since it inverts the
problem where the UI gets out of sync in all non-IE browsers to now make
it so it only gets out of sync in IE.

Bug: 48171

Change-Id: I5f279b06b098960be7bd4ad3f5e6f74b67e31d1a
2013-05-06 22:57:35 +00:00

472 lines
13 KiB
JavaScript

/*!
* VisualEditor DataModel AnnotationSet class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Annotation set.
*
* @constructor
* @param {ve.dm.IndexValueStore} store Index-value store
* @param {number[]} [indexes] Array of indexes into the store
*/
ve.dm.AnnotationSet = function VeDmAnnotationSet( store, indexes ) {
// Parent constructor
this.store = store;
this.storeIndexes = indexes || [];
};
/* Methods */
/**
* Get the index-value store.
*
* @method
* @returns {ve.dm.IndexValueStore} Index-value store
*/
ve.dm.AnnotationSet.prototype.getStore = function () {
return this.store;
};
/**
* Get a clone.
*
* @method
* @returns {ve.dm.AnnotationSet} Copy of annotation set
*/
ve.dm.AnnotationSet.prototype.clone = function () {
return new ve.dm.AnnotationSet( this.getStore(), this.storeIndexes.slice( 0 ) );
};
/**
* Get an annotation set containing only annotations within the set with a specific name.
*
* @method
* @param {string} name Type name
* @returns {ve.dm.AnnotationSet} Copy of annotation set
*/
ve.dm.AnnotationSet.prototype.getAnnotationsByName = function ( name ) {
return this.filter( function ( annotation ) { return annotation.name === name; } );
};
/**
* Get an annotation set containing only annotations within the set which are comparable
* to a specific annotation.
*
* @method
* @param {ve.dm.Annotation} annotation Annotation to compare to
* @returns {ve.dm.AnnotationSet} Copy of annotation set
*/
ve.dm.AnnotationSet.prototype.getComparableAnnotations = function ( annotation ) {
return this.filter( function ( a ) {
return ve.compareObjects(
annotation.getComparableObject(),
a.getComparableObject()
);
} );
};
/**
* Check if any annotations in the set have a specific name.
*
* @method
* @param {string} name Type name
* @returns {boolean} Annotation of given type exists in the set
*/
ve.dm.AnnotationSet.prototype.hasAnnotationWithName = function ( name ) {
return this.containsMatching( function ( annotation ) { return annotation.name === name; } );
};
/**
* Get an annotation or all annotations from the set.
*
* set.get( 5 ) returns the annotation at index 5, set.get() returns an array with all annotations
* in the entire set.
*
* @method
* @param {number} [index] If set, only get the annotation at the index
* @returns {Array|ve.dm.Annotation|undefined} The annotation at index, or an array of all
* annotation in the set
*/
ve.dm.AnnotationSet.prototype.get = function ( index ) {
if ( index !== undefined ) {
return this.getStore().value( this.getIndex( index ) );
} else {
return this.getStore().values( this.getIndexes() );
}
};
/**
* Get store index from offset within annotation set.
* @param {number} offset Offset within annotation set
* @returns {number} Store index at specified offset
*/
ve.dm.AnnotationSet.prototype.getIndex = function ( offset ) {
return this.storeIndexes[offset];
};
/**
* Get all store indexes.
* @returns {Array} Store indexes
*/
ve.dm.AnnotationSet.prototype.getIndexes = function () {
return this.storeIndexes;
};
/**
* Get the length of the set.
*
* @method
* @returns {number} The number of annotations in the set
*/
ve.dm.AnnotationSet.prototype.getLength = function () {
return this.storeIndexes.length;
};
/**
* Check if the set is empty.
*
* @method
* @returns {boolean} The set is empty
*/
ve.dm.AnnotationSet.prototype.isEmpty = function () {
return this.getLength() === 0;
};
/**
* Check whether a given annotation occurs in the set.
*
* Annotations are compared by store index.
*
* @method
* @param {ve.dm.Annotation} annotation Annotation
* @returns {boolean} There is an annotation in the set with the same hash as annotation
*/
ve.dm.AnnotationSet.prototype.contains = function ( annotation ) {
return this.indexOf( annotation ) !== -1;
};
/**
* Check whether a given store index occurs in the set.
*
* @method
* @param {number} annotation Index of annotation in the store
* @returns {boolean} There is an annotation in the set with this store index
*/
ve.dm.AnnotationSet.prototype.containsIndex = function ( index ) {
return ve.indexOf( index , this.getIndexes() ) !== -1;
};
/**
* Check whether the set contains any of the annotations in another set.
*
* @method
* @param {ve.dm.AnnotationSet} set Set to compare the set with
* @returns {boolean} There is at least one annotation in set that is also in the set
*/
ve.dm.AnnotationSet.prototype.containsAnyOf = function ( set ) {
var i, length, setIndexes = set.getIndexes(), thisIndexes = this.getIndexes();
for ( i = 0, length = setIndexes.length; i < length; i++ ) {
if ( ve.indexOf( setIndexes[i], thisIndexes ) !== -1 ) {
return true;
}
}
return false;
};
/**
* Check whether the set contains all of the annotations in another set.
*
* @method
* @param {ve.dm.AnnotationSet} set Set to compare the set with
* @returns {boolean} All annotations in set are also in the set
*/
ve.dm.AnnotationSet.prototype.containsAllOf = function ( set ) {
var i, length, setIndexes = set.getIndexes(), thisIndexes = this.getIndexes();
for ( i = 0, length = setIndexes.length; i < length; i++ ) {
if ( ve.indexOf( setIndexes[i], thisIndexes ) === -1 ) {
return false;
}
}
return true;
};
/**
* Get the index of a given annotation in the set.
*
* @method
* @param {ve.dm.Annotation} annotation Annotation to search for
* @returns {number} Index of annotation in the set, or -1 if annotation is not in the set.
*/
ve.dm.AnnotationSet.prototype.indexOf = function ( annotation ) {
return ve.indexOf( this.store.indexOfHash( ve.getHash( annotation ) ), this.getIndexes() );
};
/**
* Filter the set by an item property.
*
* This returns a new set with all annotations in the set for which the callback returned true for.
*
* @method
* @param {Function} callback Function that takes an annotation and returns boolean true to include
* @param {boolean} [returnBool] For internal use only
* @returns {ve.dm.AnnotationSet} New set containing only the matching annotations
*/
ve.dm.AnnotationSet.prototype.filter = function ( callback, returnBool ) {
var i, length, result, storeIndex, annotation;
if ( !returnBool ) {
result = this.clone();
// TODO: Should we be returning this on all methods that modify the original? Might help
// with chainability, but perhaps it's also confusing because most chainable methods return
// a new hash set.
result.removeAll();
}
for ( i = 0, length = this.getLength(); i < length; i++ ) {
storeIndex = this.getIndex( i );
annotation = this.getStore().value( storeIndex );
if ( callback( annotation ) ) {
if ( returnBool ) {
return true;
} else {
result.storeIndexes.push( storeIndex );
}
}
}
return returnBool ? false : result;
};
/**
* Check if the set contains an annotation comparable to the specified one.
*
* getComparableObject is used to compare the annotations, and should return
* true if an annotation is found which is mergeable with the specified one.
*
* @param {ve.dm.Annotation} annotation Annotation to compare to
* @returns {boolean} At least one comprable annotation found
*/
ve.dm.AnnotationSet.prototype.containsComparable = function ( annotation ) {
return this.filter( function ( a ) {
return ve.compareObjects(
annotation.getComparableObject(),
a.getComparableObject()
);
}, true );
};
/**
* Check if the set contains at least one annotation where a given property matches a given filter.
*
* This is equivalent to (but more efficient than) `!this.filter( .. ).isEmpty()`.
*
* @see ve.dm.AnnotationSet#filter
*
* @method
* @param {Function} callback Function that takes an annotation and returns boolean true to include
* @returns {boolean} At least one matching annotation found
*/
ve.dm.AnnotationSet.prototype.containsMatching = function ( callback ) {
return this.filter( callback, true );
};
/**
* Check if the set contains the same annotations as another set.
*
* Compares annotations by their comparable object value.
*
* @method
* @param {ve.dm.AnnotationSet} annotationSet The annotationSet to compare this one to
* @returns {boolean} The annotations are the same
*/
ve.dm.AnnotationSet.prototype.compareTo = function ( annotationSet ) {
var i, indexes = this.getIndexes(), length = indexes.length;
if ( length === annotationSet.getLength() ) {
for ( i = 0; i < length; i++ ) {
if ( !annotationSet.containsComparable( this.get( i ) ) ) {
return false;
}
}
} else {
return false;
}
return true;
};
/**
* Add an annotation to the set.
*
* If the annotation is already present in the set, nothing happens.
*
* The annotation will be inserted before the annotation that is currently at the given index. If index is
* negative, it will be counted from the end (i.e. index -1 is the last item, -2 the second-to-last,
* etc.). If index is out of bounds, the annotation will be added to the end of the set.
*
* @method
* @param {ve.dm.Annotation} annotation Annotation to add
* @param {number} index Index to add the annotation at
*/
ve.dm.AnnotationSet.prototype.add = function ( annotation, index ) {
var storeIndex = this.getStore().index( annotation );
// negative offset
if ( index < 0 ) {
index = this.getLength() + index;
}
// greater than length, add to end
if ( index >= this.getLength() ) {
this.push( annotation );
return;
}
// if not in set already, splice in place
if ( !this.containsIndex( storeIndex ) ) {
this.storeIndexes.splice( index, 0, storeIndex );
}
};
/**
* Add all annotations in the given set to the end of the set.
*
* Annotations from the other set that are already in the set will not be added again.
*
* @method
* @param {ve.dm.AnnotationSet} set Set to add to the set
*/
ve.dm.AnnotationSet.prototype.addSet = function ( set ) {
this.storeIndexes = ve.simpleArrayUnion( this.getIndexes(), set.getIndexes() );
};
/**
* Add an annotation at the end of the set.
*
* If the annotation is already present in the set, nothing happens.
*
* @method
* @param {ve.dm.Annotation} annotation Annotation to add
*/
ve.dm.AnnotationSet.prototype.push = function ( annotation ) {
var storeIndex = this.getStore().index( annotation );
if ( !this.containsIndex( storeIndex ) ) {
this.storeIndexes.push( storeIndex );
}
};
/**
* Remove the annotation at a given index.
*
* @method
* @param {number} index Index to remove item at. If negative, the counts from the end, see add()
* @throws {Error} Index out of bounds.
*/
ve.dm.AnnotationSet.prototype.removeAt = function ( index ) {
if ( index < 0 ) {
index = this.getLength() + index;
}
if ( index >= this.getLength() ) {
throw new Error( 'Index out of bounds' );
}
this.storeIndexes.splice( index, 1 );
};
/**
* Remove a given annotation from the set.
*
* If the annotation isn't in the set, nothing happens.
*
* @method
* @param {ve.dm.Annotation} annotation Annotation to remove
*/
ve.dm.AnnotationSet.prototype.remove = function ( annotation ) {
var index = this.indexOf( annotation );
if ( index !== -1 ) {
this.storeIndexes.splice( index, 1 );
}
};
/**
* Remove all annotations.
*
* @method
*/
ve.dm.AnnotationSet.prototype.removeAll = function () {
this.storeIndexes = [];
};
/**
* Remove all annotations in a given set from the set.
*
* Annotations that aren't in the set are ignored.
*
* @method
* @param {ve.dm.AnnotationSet} set Set to remove from the set
*/
ve.dm.AnnotationSet.prototype.removeSet = function ( set ) {
this.storeIndexes = ve.simpleArrayDifference( this.getIndexes(), set.getIndexes() );
};
/**
* Remove all annotations that are not also in a given other set from the set.
*
* @method
* @param {ve.dm.AnnotationSet} set Set to intersect with the set
*/
ve.dm.AnnotationSet.prototype.removeNotInSet = function ( set ) {
this.storeIndexes = ve.simpleArrayIntersection( this.getIndexes(), set.getIndexes() );
};
/**
* Reverse the set.
*
* This returns a copy, the original set is not modified.
*
* @method
* @returns {ve.dm.AnnotationSet} Copy of the set with the order reversed.
*/
ve.dm.AnnotationSet.prototype.reversed = function () {
var newSet = this.clone();
newSet.storeIndexes.reverse();
return newSet;
};
/**
* Merge another set into the set.
*
* This returns a copy, the original set is not modified.
*
* @method
* @param {ve.dm.AnnotationSet} set Other set
* @returns {ve.dm.AnnotationSet} Set containing all annotations in the set as well as all annotations in set
*/
ve.dm.AnnotationSet.prototype.mergeWith = function ( set ) {
var newSet = this.clone();
newSet.addSet( set );
return newSet;
};
/**
* Get the difference between the set and another set.
*
* @method
* @param {ve.dm.AnnotationSet} set Other set
* @returns {ve.dm.AnnotationSet} New set containing all annotations that are in the set but not in set
*/
ve.dm.AnnotationSet.prototype.diffWith = function ( set ) {
var newSet = this.clone();
newSet.removeSet( set );
return newSet;
};
/**
* Get the intersection of the set with another set.
*
* @method
* @param {ve.dm.AnnotationSet} set Other set
* @returns {ve.dm.AnnotationSet} New set containing all annotations that are both in the set and in set
*/
ve.dm.AnnotationSet.prototype.intersectWith = function ( set ) {
var newSet = this.clone();
newSet.removeNotInSet( set );
return newSet;
};