mediawiki-extensions-Visual.../modules/ve/dm/ve.dm.SurfaceFragment.js
Timo Tijhof f06952f2f3 Refactor ve.js utilities and improve documentation
Refactor:
* ve.indexOf
  Renamed from ve.inArray.
  This was named after the jQuery method which in turn has a longer
  story about why it is so unfortunately named. It doesn't return
  a boolean, but an index. Hence the native method being called
  indexOf as well.

* ve.bind
  Renamed from ve.proxy.
  I considered making it use Function.prototype.bind if available.
  As it performs better than $.proxy (which doesn't use to the native
  bind if available). However since bind needs to be bound itself in
  order to use it detached, it turns out with the "call()" and
  "bind()"  it is slower than the $.proxy shim:
  http://jsperf.com/function-bind-shim-perf
  It would've been like this:
  ve.bind = Function.prototype.bind ?
      Function.prototype.call.bind( Function.prototype.bind ) :
      $.proxy;
  But instead sticking to ve.bind = $.proxy;

* ve.extendObject
  Documented the parts of jQuery.extend that we use. This makes it
  easier to replace in the future.

Documentation:
* Added function documentation blocks.
* Added annotations to  functions that we will be able to remove
  in the future in favour of the native methods.
  With "@until + when/how".
  In this case "ES5". Meaning, whenever we drop support for browsers
  that don't support ES5. Although in the developer community ES5 is
  still fairly fresh, browsers have been aware for it long enough
  that thee moment we're able to drop it may be sooner than we think.
  The only blocker so far is IE8. The rest of the browsers have had
  it long enough that the traffic we need to support of non-IE
  supports it.

Misc.:
* Removed 'node: true' from .jshintrc since Parsoid is no longer in
  this repo and thus no more nodejs files.
 - This unraveled two lint errors: Usage of 'module' and 'console'.
   (both were considered 'safe globals' due to nodejs, but not in
   browser code).

* Replaced usage (before renaming):
 - $.inArray -> ve.inArray
 - Function.prototype.bind -> ve.proxy
 - Array.isArray -> ve.isArray
 - [].indexOf -> ve.inArray
 - $.fn.bind/live/delegate/unbind/die/delegate -> $.fn.on/off

Change-Id: Idcf1fa6a685b6ed3d7c99ffe17bd57a7bc586a2c
2012-08-12 20:32:45 +02: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.bind( 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;
};