mediawiki-extensions-Visual.../modules/ve/dm/ve.dm.SurfaceFragment.js
Trevor Parscal 72bddc03f0 Added basic ve.dm.SurfaceFragment class
Lots left to do here, but this is the future API for working with documents

Change-Id: I7b7e705ad4d922f37c327042051634a61dc8be66
2012-08-09 16:18:13 -07:00

341 lines
8.8 KiB
JavaScript

/**
* 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
surface.on( 'transact', ve.proxy( this, this.onTransact ) );
// 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;
};