2012-08-02 00:59:38 +00:00
|
|
|
/**
|
|
|
|
* VisualEditor data model Fragment class.
|
|
|
|
*
|
|
|
|
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
|
|
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* DataModel document fragment.
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @constructor
|
|
|
|
* @param {ve.dm.Surface} surface Target surface
|
|
|
|
* @param {ve.Range} [range] Range within target document, current selection used by default
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment = function ( surface, range ) {
|
|
|
|
// Short-circuit for null fragment
|
|
|
|
if ( !surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Properties
|
|
|
|
this.surface = surface;
|
|
|
|
this.document = surface.getDocument();
|
|
|
|
this.range = range && range instanceof ve.Range ? range : surface.getSelection();
|
|
|
|
|
|
|
|
// Events
|
2012-08-11 08:14:56 +00:00
|
|
|
surface.on( 'transact', ve.bind( this, this.onTransact ) );
|
2012-08-02 00:59:38 +00:00
|
|
|
|
|
|
|
// Initialization
|
|
|
|
var length = this.document.getLength();
|
|
|
|
this.range = new ve.Range(
|
|
|
|
// Clamp range to valid document offsets
|
|
|
|
Math.min( Math.max( this.range.from, 0 ), length ),
|
|
|
|
Math.min( Math.max( this.range.to, 0 ), length )
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Methods */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Responds to transactions being processed on the document.
|
|
|
|
*
|
|
|
|
* This keeps the range of this fragment valid, even while other transactions are being processed.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {ve.dm.Transaction} tx Transaction that's just been processed
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.onTransact = function( tx ) {
|
|
|
|
this.range = tx.translateRange( this.range );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if this is a null fragment.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {Boolean} Fragment is a null fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.isNull = function() {
|
|
|
|
return this.surface === undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a new fragment with an adjusted position
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {Number} [start] Adjustment for start position
|
|
|
|
* @param {Number} [end] Adjustment for end position
|
|
|
|
* @returns {ve.dm.SurfaceFragment} Adjusted fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.adjustRange = function ( start, end ) {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
return new ve.dm.SurfaceFragment(
|
|
|
|
this.document,
|
|
|
|
new ve.Range( this.range.start + ( start || 0 ), this.range.end + ( end || 0 ) )
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a new fragment with a zero-length selection at the start offset.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {ve.dm.SurfaceFragment} Collapsed fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.collapseRange = function () {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
return new ve.dm.SurfaceFragment( this.document, new ve.Range( this.range.start ) );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a new fragment with a range that no longer includes leading and trailing whitespace.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {ve.dm.SurfaceFragment} Trimmed fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.trimRange = function () {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
return new ve.dm.SurfaceFragment(
|
|
|
|
this.document, this.document.trimOuterSpaceFromRange( this.range )
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a new fragment that covers an expanded range of the document.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {String} [scope=parent] Method of expansion:
|
|
|
|
* 'root': Expands to cover the entire document
|
|
|
|
* 'siblings': Expands to cover all sibling nodes
|
|
|
|
* 'closest': Expands to cover the closest common ancestor node of a specified type
|
|
|
|
* 'parent': Expands to cover the closest common parent node
|
|
|
|
* @param {String} [type] Node type to use with certain scope methods that require it
|
|
|
|
* @returns {ve.dm.SurfaceFragment} Expanded fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.expandRange = function ( scope, type ) {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
var range, node, nodes, parent;
|
|
|
|
switch ( scope || 'parent' ) {
|
|
|
|
case 'root':
|
|
|
|
range = new ve.Range( 0, this.document.getData().length );
|
|
|
|
break;
|
|
|
|
case 'siblings':
|
|
|
|
// Grow range to cover all siblings
|
|
|
|
nodes = this.document.selectNodes( this.range, 'siblings' );
|
|
|
|
if ( nodes.length === 1 ) {
|
|
|
|
range = nodes[0].node.getOuterRange();
|
|
|
|
} else {
|
|
|
|
range = new ve.Range(
|
|
|
|
nodes[0].node.getOuterRange().start,
|
|
|
|
nodes[nodes.length - 1].node.getOuterRange().end
|
|
|
|
);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'closest':
|
|
|
|
// Grow range to cover closest common ancestor node of given type
|
|
|
|
node = this.document.selectNodes( this.range, 'siblings' )[0].node;
|
|
|
|
parent = node.getParent();
|
|
|
|
while ( parent.getType() !== type ) {
|
|
|
|
node = parent;
|
|
|
|
parent = parent.getParent();
|
|
|
|
if ( !parent ) {
|
|
|
|
return new ve.dm.SurfaceFragment( null );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
range = parent.getOuterRange();
|
|
|
|
break;
|
|
|
|
case 'parent':
|
|
|
|
// Grow range to cover the closest common parent node
|
|
|
|
node = this.document.selectNodes( this.range, 'siblings' )[0].node;
|
|
|
|
parent = node.getParent();
|
|
|
|
if ( !parent ) {
|
|
|
|
return new ve.dm.SurfaceFragment( null );
|
|
|
|
}
|
|
|
|
range = parent.getOuterRange();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error( 'Invalid scope argument: ' + scope );
|
|
|
|
}
|
|
|
|
return new ve.dm.SurfaceFragment( this.document, range );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get data for the fragment.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {Boolean} [deep] Get a deep copy of the data
|
|
|
|
* @returns {Array} Fragment data
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.getData = function ( deep ) {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return this.document.getData( this.range, deep );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get plain text for the fragment.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {Array} Fragment text
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.getText = function () {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
var i, length,
|
|
|
|
text = '',
|
|
|
|
data = this.document.getData( this.range );
|
|
|
|
for ( i = 0, length = data.length; i < length; i++ ) {
|
|
|
|
if ( data[i].type === undefined ) {
|
|
|
|
// Annotated characters have a string at index 0, plain characters are 1-char strings
|
|
|
|
text += typeof data[i] === 'string' ? data[i] : data[i][0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return text;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get nodes covered by the fragment.
|
|
|
|
*
|
|
|
|
* @see {ve.Document.selectNodes} for information about the modes argument.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {String} [mode='leaves'] Type of selection to perform
|
|
|
|
* @returns {Array} List of nodes and related information
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.getNodes = function ( mode ) {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return this.document.selectNodes( mode, this.range );
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Applies an annotation to content in the fragment.
|
|
|
|
*
|
|
|
|
* To avoid problems identified in bug 33108, use the {ve.dm.SurfaceFragment.trimRange} method.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {String} method Mode of annotation, either 'set' or 'clear'
|
|
|
|
* @param {String} type Annotation type, for example: 'textStyle/bold'
|
|
|
|
* @param {Object} [data] Additional annotation data
|
|
|
|
* @returns {ve.dm.SurfaceFragment} This fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.annotateContent = function ( method, type, data ) {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
var tx,
|
|
|
|
annotation = { 'type': type };
|
|
|
|
if ( data ) {
|
|
|
|
annotation.data = data;
|
|
|
|
}
|
|
|
|
if ( this.range.getLength() ) {
|
|
|
|
tx = ve.dm.Transaction.newFromAnnotation( this.document, this.range, method, annotation );
|
|
|
|
this.surface.change( tx, this.range );
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove content in the fragment and insert content before it.
|
|
|
|
*
|
|
|
|
* This will move the fragment to the end of the insertion and make it zero-length.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {Mixed} content Content to insert
|
|
|
|
* @param {Boolean} annotate Content should be automatically annotated to match surrounding content
|
|
|
|
* @returns {ve.dm.SurfaceFragment} This fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.insertContent = function ( content, annotate ) {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
// TODO: Implement
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes content in the fragment.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {ve.dm.SurfaceFragment} This fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.removeContent = function () {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
// TODO: Implement
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts content branches in the fragment.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {String} type Element type to convert to
|
|
|
|
* @param {Object} [attributes] Initial attributes for new element
|
|
|
|
* @returns {ve.dm.SurfaceFragment} This fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.convertNodes = function ( type, attributes ) {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
// TODO: Implement
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wraps content in the fragment with one or more elements.
|
|
|
|
*
|
|
|
|
* TODO: Figure out what the arguments for this function should be
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {ve.dm.SurfaceFragment} This fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.wrapNodes = function () {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
// TODO: Implement
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unwraps content in the fragment out of one or more elements.
|
|
|
|
*
|
|
|
|
* TODO: Figure out what the arguments for this function should be
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @returns {ve.dm.SurfaceFragment} This fragment
|
|
|
|
*/
|
|
|
|
ve.dm.SurfaceFragment.prototype.unwrapNodes = function () {
|
|
|
|
// Handle null fragment
|
|
|
|
if ( !this.surface ) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
// TODO: Implement
|
|
|
|
return this;
|
|
|
|
};
|